# Siren — full content dump Concatenation of every non-draft documentation page, recipe description, feature page, integration page, and integration program guide on sirenaffiliates.com. Generated at build time; do not edit by hand. Comparison pages are not duplicated here: fetch https://www.sirenaffiliates.com/compare/index.json for the verdict facts of every Siren-vs-competitor comparison. --- # Documentation ## 0.10.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/0-10-0 Introduces engagement expiration, which makes it possible to ignore stale engagements when a conversion is created. - Introduces engagement expiration, which makes it possible to ignore stale engagements when a conversion is created. ## 0.10.1 Source: https://www.sirenaffiliates.com/documentation/changelogs/0-10-1 Fixes an issue where Un-ticking incentive structure settings was not updating properly. - Fixes an issue where Un-ticking incentive structure settings was not updating properly. ## 0.9.2 Source: https://www.sirenaffiliates.com/documentation/changelogs/0-9-2 Resolves a critical error when assigning a coupon directly from a URL. Clarifies wording when updating the primary Siren account page. - Resolves a critical error when assigning a coupon directly from a URL. - Clarifies wording when updating the primary Siren account page. ## 1.0.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-0-2 Fixed a bug that prevented collaborators from seeing their dashboard when they had a different role. Fixed a bug that prevented autoupdater from functioning as-expected - Fixed a bug that prevented collaborators from seeing their dashboard when they had a different role. - Fixed a bug that prevented autoupdater from functioning as-expected ## 1.0.1 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-1-2 Fixes a bug that caused an unexpected error in older PHP versions - Fixes a bug that caused an unexpected error in older PHP versions ## 1.0.2 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-2 Fixes a bug that prevented obligation IDs from being set on some hosts. - Fixes a bug that prevented obligation IDs from being set on some hosts. ## 1.0.3 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-3 Fixed an issue that caused unexpected issues on older versions of PHP - Fixed an issue that caused unexpected issues on older versions of PHP ## 1.0.4 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-4 Fixed an issue that caused unexpected issues on older versions of PHP - Fixed an issue that caused unexpected issues on older versions of PHP ## 1.0.5 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-5 Documentation for 1.0.5 - Fixes an issue where new collaborators were not being associated with existing users on-creation. - Fixes an issue where collaborator tracking codes were not being saved when creating collaborators. ## 1.0.7 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-7 Fixes issue that causes unmapped orders to throw an execption - Fixes issue that causes unmapped orders to throw an execption ## 1.0.8 Source: https://www.sirenaffiliates.com/documentation/changelogs/1-0-8 Fixes bug that caused transaction calculations to be incorrectly calculated with multiple quantities. - Fixes bug that caused transaction calculations to be incorrectly calculated with multiple quantities. ## 2.0.1 Source: https://www.sirenaffiliates.com/documentation/changelogs/2-0-1 What’s Changed Fix for an uncaught error when activating a Siren license. Resolution of a PHPNomad MySQL syntax error. Update to force the opportunity cookie setting to default to JavaScript Full Changelog: https://github.com/Novatorius/siren-wordpress/compare/2.0.0…2.0.1 ## What’s Changed - Fix for an uncaught error when activating a Siren license. - Resolution of a PHPNomad MySQL syntax error. - Update to force the opportunity cookie setting to default to JavaScript **Full Changelog**: https://github.com/Novatorius/siren-wordpress/compare/2.0.0…2.0.1 ## 2.0.2 Source: https://www.sirenaffiliates.com/documentation/changelogs/2-0-2 What’s Changed Bump siren 2.0.2 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/21 Fixed https://github.com/Novatorius/api-manager-integration/pull/10 Full Changelog: https://github.com/Novatorius/siren-wordpress/compare/2.0.1…2.0.2 ## What’s Changed - Bump siren 2.0.2 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/21 - Fixed https://github.com/Novatorius/api-manager-integration/pull/10 **Full Changelog**: https://github.com/Novatorius/siren-wordpress/compare/2.0.1…2.0.2 ## Activity Feeds Source: https://www.sirenaffiliates.com/documentation/general/activity-feeds The activity feed is Siren's running history of what's happened to a record. This page explains where it appears, what writes to it, and how to use it. The activity feed is the running history that appears on every major detail screen in Siren. Open a [collaborator](/documentation/general/what-is-a-collaborator), a [conversion](/documentation/general/what-is-a-conversion), an [obligation](/documentation/general/what-are-obligations), a [fulfillment](/documentation/general/what-is-a-fulfillment), an [engagement](/documentation/general/what-is-an-engagement), a [transaction](/documentation/general/what-are-transactions), or a [collaborator group](/documentation/general/what-are-collaborator-groups) and the activity feed shows you, in chronological order, everything Siren has recorded about that record. Each entry in the feed is a note. A note is a single timestamped record of something that happened, like "an obligation was issued for this conversion" or "this payout was marked paid." The feed for any given record is the collection of notes that are linked to it, oldest at the bottom, newest at the top. ## What writes to the feed You don't write notes by hand. Siren writes them automatically as things happen across the system. When an obligation is issued, a conversion is approved, a payout is created, a refund is triggered, or anything else worth remembering occurs, Siren records a note and surfaces it wherever it's relevant. That last part is the point. The same event often touches several records at once. A payout involves a collaborator, an obligation, a fulfillment, and a transaction, and the note shows up on every one of their activity feeds. You don't have to track down which record the story lives on. Open whichever one you're already looking at and it's there. [Collaborator groups](/documentation/general/what-are-collaborator-groups) have their own feed too. Creating a group, renaming it, changing its structure, deleting it, adding or removing members, changing a member's metadata (such as reordering or reparenting that member within the group), and binding or unbinding it to a Program or Distributor all write notes to the group's feed. Membership changes are recorded in two places. When a collaborator is added to a group, removed from a group, or has their membership metadata changed, the note appears on both the group's feed and that collaborator's feed, so you can see a collaborator's group history from their own detail screen. ## What the feed is for The activity feed exists to answer the question "what happened to this record?" without having to piece together the story from multiple screens or reach for a developer to check the logs. A collaborator emails in asking why a conversion they expected to see hasn't paid out yet. Open the conversion, scan the feed, and the answer is usually right there. Maybe the conversion was rejected. Maybe the obligation was issued but hasn't been rolled into a fulfillment yet. Maybe a refund came through and reversed it. A bookkeeping reconciliation doesn't match what you sent out last cycle. Open the fulfillment, read the feed, and every obligation that was included and every payout that was generated is on the record with a timestamp. A program manager wants to spot-check that the incentive pipeline is working correctly after a configuration change. Open a recent conversion and the feed shows the full sequence of events that led from the engagement to the obligation. The feed is also where you'll catch silent failures. If a conversion was approved but no obligation appears in its feed, something in the pipeline didn't fire as expected, and that absence is often the first signal. ## How to read an entry Each entry is a short, plain-language description of the event, a timestamp, and links to the related records. An obligation-issued note on a collaborator's feed will link to both the obligation and the conversion that caused it, so you can click straight through to either. Notes don't carry editorial content or commentary. They're a factual record of what Siren did, not a place for manual annotation. > **For developers:** The activity feed is backed by the [notes resource](/documentation/resource-reference/notes). Each entry is a note record stored with a blueprint key (for example, `obligation_issued`) and resolved into rendered content at read time. The REST API exposes notes through `GET /notes` with a `sourceType` and `sourceId` filter. See the [resource reference](/documentation/resource-reference/notes) for field selection, the available blueprint keys, and how the resolver system hydrates notes into display content. ## Adapters Source: https://www.sirenaffiliates.com/documentation/extensions/adapters Converting plugin-specific data formats into Siren's standardized transaction detail structure. # Adapters Adapters convert plugin-specific data formats into Siren's standardized structures. While transformers handle *events* (deciding whether and when to fire a domain event), adapters handle *data* (converting the shape of platform-specific objects into the shape Siren's domain layer expects). The primary adapter in every commerce extension is `OrderToTransactionDetailsAdapter`, which converts an order's line items, taxes, shipping, discounts, and fees into a standardized array format that the `SaleTriggered` and `RenewalTriggered` events carry into the domain layer. ## How do adapters differ from transformers? | Concern | Transformer | Adapter | |---------|------------|---------| | Scope | Decides *whether* to fire a domain event | Converts *data formats* for domain consumption | | Input | WordPress hook parameters (order ID, etc.) | Platform-specific data objects (order, line items) | | Output | Domain event instance or `null` | Standardized data array | | Business logic | Opportunity location, duplicate prevention | Price conversion, type mapping, attribute extraction | | Called by | The framework (via event binding callbacks) | Transformers (as a dependency) | A transformer *uses* an adapter. The typical flow: ``` WordPress hook fires -> Transformer receives hook parameters -> Transformer checks opportunity + duplicates -> Transformer calls adapter to convert order data -> Transformer constructs domain event with adapted data ``` ## What format should transaction details use? Each line item is represented as an associative array with these fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Display name of the item | | `description` | `string` | Yes | Longer description | | `type` | `string` | Yes | Item type (see below) | | `value` | `int` | Yes | Price per unit in cents | | `quantity` | `int` | Yes | Number of units | | `units` | `string` | Yes | Currency code (e.g., `'USD'`, `'EUR'`) | | `externalId` | `string\|null` | No | Platform-specific product/item ID | | `attributes` | `array` | No | Additional metadata (collaborators, categories, SKU) | ### Item Types | Type | Description | |------|-------------| | `product` | A standard product line item | | `subscription` | A subscription product (WooCommerce Subscriptions) | | `shipping` | Shipping charges | | `tax` | Tax charges | | `discount` | Discount amount (value is negative) | | `fee` | Additional fees (signup fees, service fees, etc.) | ### Price Format: Cents as Integers All monetary values must be integers representing the smallest currency unit (cents for USD/EUR, pence for GBP, etc.). A $29.99 item has a value of `2999`. Siren provides `FloatToIntPriceAdapter` to handle the conversion: ```php use Siren\Commerce\Adapters\FloatToIntPriceAdapter; $priceAdapter = new FloatToIntPriceAdapter(); $priceAdapter->toInt(29.99); // Returns 2999 $priceAdapter->toInt(0.50); // Returns 50 $priceAdapter->toFloat(2999); // Returns 29.99 ``` The adapter multiplies by 100 and casts to int. Always use this adapter rather than doing the math yourself to ensure consistent rounding behavior. ### Discounts Are Negative Discount line items use a negative value to indicate a reduction: ```php $discount = $this->priceAdapter->toInt($order->get_discount_total()); if ($discount > 0) { $result[] = [ 'name' => 'Discount', 'description' => 'Discount Total', 'type' => 'discount', 'value' => $discount * -1, // Negative value 'quantity' => 1, 'units' => $currency, 'externalId' => null ]; } ``` ## How does the WooCommerce adapter work? The WooCommerce `OrderToTransactionDetailsAdapter` shows the complete pattern. Here is its `toArray()` method broken down by section: ### Constructor and Dependencies ```php class OrderToTransactionDetailsAdapter { protected FloatToIntPriceAdapter $priceAdapter; protected MappingDatastore $mappings; protected LoggerStrategy $logger; public function __construct( FloatToIntPriceAdapter $priceAdapter, MappingDatastore $mappings, LoggerStrategy $logger ) { $this->mappings = $mappings; $this->priceAdapter = $priceAdapter; $this->logger = $logger; } } ``` The adapter depends on `FloatToIntPriceAdapter` for price conversion, `MappingDatastore` for looking up product-to-collaborator relationships, and `LoggerStrategy` for exception logging. ### Shipping ```php $shippingTotal = $this->priceAdapter->toInt($order->get_shipping_total()); if ($shippingTotal > 0) { $result[] = [ 'name' => 'Shipping', 'description' => 'Shipping Fees', 'type' => 'shipping', 'value' => $shippingTotal, 'quantity' => 1, 'units' => $currency, 'externalId' => null ]; } ``` Shipping is a single line item. If the order has no shipping cost, it is omitted entirely (not included with a zero value). ### Taxes ```php $totalTax = $this->priceAdapter->toInt($order->get_total_tax()); if ($totalTax > 0) { $result[] = [ 'name' => 'Taxes', 'description' => 'Total taxes', 'type' => 'tax', 'value' => $totalTax, 'quantity' => 1, 'units' => $currency, 'externalId' => null ]; } ``` ### Product Line Items This is the most complex section — each product includes attributes for collaborator mappings and categories: ```php foreach ($lineItems as $id => $item) { $total = $this->priceAdapter->toInt($item->get_total()); $product = $item->get_product(); if ($product && $total > 0) { $terms = get_the_terms($product->get_id(), 'product_cat'); $categories = $terms ? Arr::pluck($terms, 'slug') : []; $type = class_exists(WC_Subscriptions_Product::class) && WC_Subscriptions_Product::is_subscription($product) ? 'subscription' : 'product'; $result[] = [ 'name' => $item->get_name(), 'externalId' => $product->get_id(), 'description' => $item->get_quantity() . ' X Product ' . $item->get_name(), 'type' => $type, 'value' => $total / $item->get_quantity(), // Per-unit price 'quantity' => $item->get_quantity(), 'units' => $currency, 'attributes' => [ 'collaborators' => $this->getProductCollaborators($item->get_product_id()), 'categories' => $categories, 'sku' => $product->get_sku() ? $product->get_sku() : null ] ]; } } ``` The `value` field is per-unit, not the line total — if a customer buys 3 items at $30 total, each item's value is `1000` (= $10.00 in cents). The `type` is dynamic: products are typed as `'subscription'` when WooCommerce Subscriptions detects them as subscription products, otherwise `'product'`. And `externalId` is the WooCommerce product ID, used for mapping back to the platform. ### Fees ```php foreach ($fees as $item) { $total = $this->priceAdapter->toInt($item->get_total()); if ($total > 0) { $result[] = [ 'name' => $item->get_name(), 'description' => $item->get_name(), 'type' => 'fee', 'value' => $total, 'quantity' => 1, 'units' => $currency, 'attributes' => [ 'sku' => null ] ]; } } ``` ## How do product-to-collaborator mappings work? One of the most important adapter responsibilities is including collaborator mappings. When a store owner assigns specific affiliates (collaborators) to specific products, those assignments are stored as mappings. The adapter looks them up: ```php protected function getProductCollaborators($productId): array { try { return Arr::pluck($this->mappings->andWhere([ ['column' => 'externalId', 'operator' => '=', 'value' => $productId], ['column' => 'localType', 'operator' => '=', 'value' => 'collaborator'], ['column' => 'externalType', 'operator' => '=', 'value' => 'wc_product'] ]), 'localId'); } catch (DatastoreErrorException $e) { $this->logger->logException($e); return []; } } ``` This returns an array of collaborator IDs associated with the product. The `externalType` uses the same convention described in the Transformers article — `{extension_id}_{entity}` (e.g., `wc_product`, `edd_product`). The EDD adapter uses the same pattern with `edd_product`: ```php protected function getProductCollaborators($productId): array { try { return Arr::pluck($this->mappings->andWhere([ ['column' => 'externalId', 'operator' => '=', 'value' => $productId], ['column' => 'localType', 'operator' => '=', 'value' => 'collaborator'], ['column' => 'externalType', 'operator' => '=', 'value' => 'edd_product'], ]), 'localId'); } catch (DatastoreErrorException $e) { $this->logger->logException($e); return []; } } ``` ## How is category information extracted? Product categories are extracted from the platform's taxonomy system and included as slug arrays: ```php // WooCommerce — uses the 'product_cat' taxonomy $terms = get_the_terms($product->get_id(), 'product_cat'); $categories = $terms ? Arr::pluck($terms, 'slug') : []; // EDD — uses the 'download_category' taxonomy $categories = wp_get_post_terms($itemId, 'download_category', ['fields' => 'slugs']); ``` Categories are stored in the attributes array and used by the domain layer for category-based commission rules. ## What models store the adapter output? After the domain layer processes the adapter output, the data is stored as `TransactionDetail` and `TransactionDetailAttribute` models: ### TransactionDetail ```php final class TransactionDetail implements DataModel, HasSingleIntIdentity { protected int $transactionId; protected string $description; protected string $name; protected Amount $value; // Value object wrapping int cents + currency protected string $type; // 'product', 'shipping', 'tax', 'discount', 'fee' protected string $units; // Currency code protected int $quantity; } ``` ### TransactionDetailAttribute ```php class TransactionDetailAttribute implements DataModel { protected int $transactionDetailId; protected string $key; // 'collaborators', 'categories', 'sku' protected $value; // array or scalar } ``` The adapter output arrays map directly to these models. The `attributes` array key in the adapter output becomes multiple `TransactionDetailAttribute` records — one per key. ## How does the EDD adapter differ? The EDD adapter follows the same structure but handles EDD-specific concepts like order adjustments: ```php // EDD uses order adjustments for taxes, discounts, and credits foreach ($order->order->adjustments as $adjustment) { if ($adjustment->type === 'tax') { $result[] = [ 'name' => $adjustment->description, 'description' => $adjustment->description, 'type' => 'tax', 'value' => $this->priceAdapter->toInt($adjustment->amount), 'quantity' => 1, 'units' => $currency, 'externalId' => null, ]; } else if ($adjustment->type === 'discount') { $result[] = [ 'name' => $adjustment->description, 'description' => $adjustment->description, 'type' => 'discount', 'value' => $this->priceAdapter->toInt($adjustment->amount) * -1, 'quantity' => 1, 'units' => $currency, 'externalId' => null, ]; } // ... credits mapped to 'discount', everything else to 'fee' } ``` EDD also calculates shipping and signup fees from the payment's fee array, separating them by fee ID: ```php public function calculateFees(\EDD_Payment $order) { $shippingTotal = $signupFee = 0; if (count($order->fees) > 0) { foreach ($order->fees as $fee) { if (false !== strpos($fee['id'], 'simple_shipping')) { $shippingTotal += $fee['amount']; } elseif (false !== strpos($fee['id'], 'signup_fee')) { $signupFee += $fee['amount']; } } } return [$shippingTotal, $signupFee]; } ``` ## How do I build a new adapter? Your `toArray()` method should accept the platform order ID and retrieve the order object using the platform's API. From there, extract shipping charges as a single aggregate line item, taxes (aggregated or itemized), and discounts (always multiply by -1 for the value). For product line items, include the `externalId`, calculate per-unit price, determine the type, and populate attributes with collaborators, categories, and SKU. Include any fees (signup fees, service fees, etc.) as separate line items. Use `FloatToIntPriceAdapter` for every price conversion — never multiply manually. Query `MappingDatastore` for product-to-collaborator relationships to populate the collaborators attribute. If the order cannot be loaded, return an empty array early. The adapter should never throw exceptions. Wrap datastore calls in try/catch and return empty arrays or defaults on failure. The transformer decides whether an empty result should prevent event dispatch. ## Add Collaborator Group Members Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/add-members Bulk-adds collaborators to an existing collaborator group, skipping any that are already members. # Add Collaborator Group Members `POST /siren/v1/collaborator-groups/{id}/members` Bulk-adds one or more collaborators to an existing group. The operation is idempotent. Collaborator ids that already have a row in this group are skipped, and only the newly-added rows are returned. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `members` | object[] | No | Array of member entries to add. Each entry is `{ "collaboratorId": , "metadata": }`. Entries without a non-zero `collaboratorId` are ignored. | | `members[].collaboratorId` | integer | Yes | The collaborator to add. Required on each entry. | | `members[].metadata` | object | No | Structure-specific data for the member. Shape depends on the group's `structure` (for example `{ "position": }` for `linearChain`). Defaults to an empty object. | **Query Parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `id` | integer | Yes | Path parameter. The id of the collaborator group to add members to. | **Example Request:** ```json { "members": [ { "collaboratorId": 43, "metadata": { "position": 3 } } ] } ``` **Example Response:** ```json [ { "id": 89, "groupId": 12, "collaboratorId": 43, "metadata": { "position": 3 } } ] ``` Returns `200` on success. The response is the array of newly-added member rows. Collaborator ids that already belonged to the group are silently skipped and do not appear in the response, so an entirely redundant request returns an empty array. Responds `404` when no group matches the supplied `id`, and `500` if the members cannot be written. Each returned row includes `id`, `groupId`, `collaboratorId`, `metadata`, `dateCreated`, and `dateModified`. The example above shows the first four for brevity. The joined collaborator name, email, and status are not part of this response, so call [List Collaborator Group Members](/documentation/resource-reference/collaborator-groups/list-members) if you need those. An empty or omitted `members` array is valid and returns `200` with an empty array. The endpoint only ever creates rows, and it never updates a member who is already present, so the `metadata` you send for a collaborator who is already in the group is ignored rather than applied. To change an existing member's `position` or parent, use [Set Collaborator Group Members](/documentation/resource-reference/collaborator-groups/set-members), the full-replace endpoint. Requires authentication and the update capability on the `CollaboratorGroup` resource. **Events:** Each newly-added row broadcasts a `CollaboratorAddedToCollaboratorGroup` event for downstream listeners. ## Affiliate Tiers on Lite by Stacking Programs Source: https://www.sirenaffiliates.com/documentation/getting-started/affiliate-tiers-on-lite-by-stacking-programs How to pay some affiliates a higher rate than others on Siren Lite without ever paying two commissions on one order: a base program every affiliate is in, plus a top-up program only the higher tier is in. import RelatedDocs from "@/components/content/RelatedDocs.astro"; You want some affiliates to earn 10% and the rest to earn 5%. The obvious build is two programs, one at 5% and one at 10%, with each affiliate in one of them. On Siren Lite that build can pay both affiliates on the same order. This page explains why, and shows the build that cannot. ## Every program attributes on its own Each [program](/documentation/general/what-are-programs) decides credit by itself. When an order completes, Siren evaluates every program that could match it, and each matching program pays its own winner. Programs stack. A customer who clicked a regular affiliate's link on Monday and a top affiliate's link on Thursday has engaged with both, so a 5% program and a separate 10% program both fire, and that one order pays 15% across two people. On Essentials and above, a [program group](/documentation/general/what-are-program-groups) fixes this by letting only one program in the group fire per conversion. On Lite there are no program groups, so the fix is in how you build the programs. ## The build that never double pays Make the tiers add up instead of competing. 1. Create a base program at the lowest rate, 5% in this example, and enroll every affiliate in it, the top tier included. 2. Create a top-up program at the difference, another 5% here, and enroll only the affiliates who should earn 10%. Now walk the same order through it. The base program has every affiliate in it, so whichever engagement wins under its attribution rule, first touch or last touch, it pays 5% once. The top-up program only has the higher tier in it, so it fires only when one of them was part of the journey, and pays 5% more to that person. A top-tier affiliate who brought the sale earns 10%. A regular affiliate earns 5%. And the order never pays more than 10% in total, whoever clicked what, because the base program picks one winner and the top-up cannot fire for anyone outside it. The rates in the top-up are the difference, not the tier's full rate. For three tiers at 5%, 10%, and 15%, that is a base program at 5% with everyone in it, a second 5% program with the middle and top tiers in it, and a third 5% program with the top tier alone. ## When to use a program group instead On Essentials, Plus, or Pro, a program group with one program per rate is simpler to read in the dashboard and gives you the group's own tie-break rules. The stacked build above still works on every plan, and it is the only safe build on Lite. ## Aliases Source: https://www.sirenaffiliates.com/documentation/resource-reference/aliases Accessing and querying collaborator aliases through the PHP data layer. Covers tracking codes, coupon codes, historical lookups, and code generation. import CodeTabs from "@/components/content/CodeTabs.astro"; # Aliases An alias in Siren is a code that identifies a collaborator in tracking links, coupon systems, or other attribution mechanisms. When a customer clicks `?ref=JNE`, Siren resolves `JNE` to a collaborator through the alias system. When a coupon code is applied at checkout, the same system connects that code to a collaborator. Aliases are scoped by type. A "tracking" alias is the code in referral URLs. A "coupon" alias is a promotional code bound to a collaborator. The system is extensible, so extensions can register additional types. Every alias record carries an issued date. When a collaborator's code changes, Siren doesn't overwrite the old record — it creates a new one with a later timestamp. This means old links and codes continue to resolve correctly, and the system can reconstruct who owned a given code at any point in history. > Aliases are created automatically when collaborators are created, and updated through the collaborator REST API. If you find yourself writing code that creates alias records directly, consider whether the collaborator create/update endpoints would be more appropriate. Direct datastore access is primarily useful for reading alias data and for migration scripts. ## Accessing alias data The alias datastore is available through dependency injection or the static facade. Dependency injection is preferred for extension code wired through an initializer. The facade is convenient for standalone scripts, theme files, or other code running outside Siren's container. ```php use Siren\Collaborators\Core\Datastores\CollaboratorAliases\Interfaces\CollaboratorAliasesDatastore; class ReferralLinkService { protected CollaboratorAliasesDatastore $aliases; public function __construct(CollaboratorAliasesDatastore $aliases) { $this->aliases = $aliases; } public function getTrackingCode(int $collaboratorId): ?string { try { $alias = $this->aliases->getAliasForCollaborator($collaboratorId, 'tracking'); return $alias->getCode(); } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { return null; } } } ``` ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; $alias = CollaboratorAliases::getAliasByCode('JNE', 'tracking'); $collaboratorId = $alias->getCollaboratorId(); ``` ## The alias model Each alias record is represented by an `Alias` model instance. | Field | Type | Description | |---|---|---| | code | string | The alias code (e.g., a tracking slug or coupon code) | | type | string | The alias category: `tracking`, `coupon`, or a custom type | | collaboratorId | int | The collaborator this alias belongs to | | issuedDate | DateTime | When this alias was assigned | Getter methods: `getCode()`, `getType()`, `getCollaboratorId()`, `getIssuedDate()`. The primary key is a compound of all four fields, which allows the same code to exist for different types and the same collaborator to have multiple historical records of the same type. ## Looking up an alias by code `getAliasByCode(string $code, string $type, ?DateTime $before = null)` finds the alias record matching a specific code and type. Since the same code can be reassigned over time, this returns the most recently issued match. Pass a `DateTime` to the `$before` parameter to find who owned the code at that point in history. Throws `RecordNotFoundException` if no matching alias exists. ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; // Who currently owns this tracking code? $alias = CollaboratorAliases::getAliasByCode('JNE', 'tracking'); $collaboratorId = $alias->getCollaboratorId(); // Who owned it on January 1st? $historicalAlias = CollaboratorAliases::getAliasByCode( 'JNE', 'tracking', new DateTime('2025-01-01') ); ``` ## Getting a collaborator's current alias `getAliasForCollaborator(int $id, string $type, ?DateTime $before = null)` retrieves the alias of a given type for a specific collaborator. Returns the most recently issued alias of that type. Like `getAliasByCode`, the `$before` parameter supports historical lookups. Throws `RecordNotFoundException` if the collaborator has no alias of that type. ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; // Get current tracking code $alias = CollaboratorAliases::getAliasForCollaborator(12, 'tracking'); echo $alias->getCode(); // e.g. "JNE" // Get coupon alias $couponAlias = CollaboratorAliases::getAliasForCollaborator(12, 'coupon'); echo $couponAlias->getCode(); // e.g. "JANE20" ``` ## How aliases are generated When a collaborator is created, a `GenerateCollaboratorTrackingAlias` listener fires and generates a unique tracking code automatically. The code generator produces consonant-only strings (from the set B, C, D, G, H, J, K, L, P, R, T, V, Z) to avoid accidentally spelling words. The length is calculated based on the number of existing aliases to minimize collisions. The minimum code length is configurable via the `collaborators.config.minimumTrackingIdLength` config value. The `AliasGenerator` interface can be replaced through the DI container if you need a different generation strategy. You can also provide a custom tracking code when creating or updating a collaborator through the REST API by passing the `trackingId` field. If the code is already taken, the API returns an error. ## How aliases are used in the attribution pipeline Aliases connect customer actions to collaborators at two points in the pipeline. When a customer visits a URL containing the referral parameter (default `?ref=CODE`), the `CollaboratorFromUrl` locator extracts the code and calls `getAliasByCode(code, 'tracking')` to resolve the collaborator. The config key `collaborator.urlReference.key` controls the parameter name. When a customer applies a coupon code and the `BoundCouponUsed` engagement trigger is active, the `CollaboratorFromAlias` locator calls `getAliasByCode(code, 'coupon')` to find the associated collaborator. If a match is found, an engagement is created for the collaborator's active programs. ## Practical examples ### Building a referral link for a collaborator ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; function getReferralUrl(int $collaboratorId, string $baseUrl): ?string { try { $alias = CollaboratorAliases::getAliasForCollaborator($collaboratorId, 'tracking'); return $baseUrl . '?ref=' . $alias->getCode(); } catch (RecordNotFoundException $e) { return null; } } ``` ### Checking if a coupon code is linked to a collaborator ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; function getCollaboratorForCoupon(string $couponCode): ?int { try { $alias = CollaboratorAliases::getAliasByCode($couponCode, 'coupon'); return $alias->getCollaboratorId(); } catch (RecordNotFoundException $e) { return null; } } ``` ### Querying all aliases for a collaborator ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; $allAliases = CollaboratorAliases::andWhere([ ['column' => 'collaboratorId', 'operator' => '=', 'value' => 12] ], null, null, 'dateIssued', 'DESC'); foreach ($allAliases as $alias) { echo $alias->getType() . ': ' . $alias->getCode() . ' (issued ' . $alias->getIssuedDate()->format('Y-m-d') . ')' . PHP_EOL; } ``` ## All REST Endpoints Source: https://www.sirenaffiliates.com/documentation/resource-reference/all-rest-endpoints Complete list of every REST API endpoint in Siren with HTTP method, path, and link to detailed documentation. # All REST Endpoints Every REST endpoint available in Siren, organized by resource. All endpoints use the base path `/wp-json/siren/v1`. Authentication is required unless otherwise noted. See the [introduction](/documentation/resource-reference/introduction) for authentication, response format, and conventions. ## Programs | Method | Path | Description | |--------|------|-------------| | GET | `/programs` | [List programs](/documentation/resource-reference/programs/list) | | GET | `/programs/:id` | [Get program](/documentation/resource-reference/programs/get-by-id) | | POST | `/programs` | [Create program](/documentation/resource-reference/programs/create) | | PATCH | `/programs/:id` | [Update program](/documentation/resource-reference/programs/update) | | DELETE | `/programs/:id` | [Delete program](/documentation/resource-reference/programs/delete) | | POST | `/programs/bulk` | [Bulk actions](/documentation/resource-reference/programs/bulk-actions) | | GET | `/programs/status-types` | [Get status types](/documentation/resource-reference/programs/status-types) | | GET | `/programs/incentive-types` | [Get incentive types](/documentation/resource-reference/programs/incentive-types) | | GET | `/programs/currency-types` | [Get currency types](/documentation/resource-reference/programs/currency-types) | ## Program Groups | Method | Path | Description | |--------|------|-------------| | GET | `/program-groups` | [List program groups](/documentation/resource-reference/program-groups/list) | | GET | `/program-groups/:id` | [Get program group](/documentation/resource-reference/program-groups/get-by-id) | | POST | `/program-groups` | [Create program group](/documentation/resource-reference/program-groups/create) | | PATCH | `/program-groups/:id` | [Update program group](/documentation/resource-reference/program-groups/update) | | DELETE | `/program-groups/:id` | [Delete program group](/documentation/resource-reference/program-groups/delete) | | POST | `/program-groups/bulk` | [Bulk actions](/documentation/resource-reference/program-groups/bulk-actions) | ## Collaborators | Method | Path | Description | |--------|------|-------------| | GET | `/collaborators` | [List collaborators](/documentation/resource-reference/collaborators/list) | | GET | `/collaborators/:id` | [Get collaborator](/documentation/resource-reference/collaborators/get-by-id) | | POST | `/collaborators` | [Create collaborator](/documentation/resource-reference/collaborators/create) | | PATCH | `/collaborators/:id` | [Update collaborator](/documentation/resource-reference/collaborators/update) | | DELETE | `/collaborators/:id` | [Delete collaborator](/documentation/resource-reference/collaborators/delete) | | GET | `/collaborators/status-types` | [Get status types](/documentation/resource-reference/collaborators/status-types) | | POST | `/collaborators/generate-referral-code` | [Generate referral code](/documentation/resource-reference/collaborators/generate-referral-code) | | POST | `/collaborators/bulk` | [Bulk actions](/documentation/resource-reference/collaborators/bulk-actions) | | POST | `/collaborators/submit-request` | [Submit collaborator request](/documentation/resource-reference/collaborators/submit-request) | | POST | `/collaborators/signup-form-jwt` | [Create signup form JWT](/documentation/resource-reference/collaborators/signup-form-jwt) | ## Collaborator Groups | Method | Path | Description | |--------|------|-------------| | GET | `/collaborator-groups` | [List groups](/documentation/resource-reference/collaborator-groups/list) | | GET | `/collaborator-groups/:id` | [Get group](/documentation/resource-reference/collaborator-groups/get-by-id) | | POST | `/collaborator-groups` | [Create group](/documentation/resource-reference/collaborator-groups/create) | | PUT | `/collaborator-groups/:id` | [Update group](/documentation/resource-reference/collaborator-groups/update) | | DELETE | `/collaborator-groups/:id` | [Delete group](/documentation/resource-reference/collaborator-groups/delete) | | GET | `/collaborator-groups/:id/members` | [List members](/documentation/resource-reference/collaborator-groups/list-members) | | POST | `/collaborator-groups/:id/members` | [Add members](/documentation/resource-reference/collaborator-groups/add-members) | | PUT | `/collaborator-groups/:id/members` | [Full-replace members](/documentation/resource-reference/collaborator-groups/set-members) | | DELETE | `/collaborator-groups/:id/members/:collaboratorId` | [Remove a member](/documentation/resource-reference/collaborator-groups/remove-member) | | GET | `/collaborator-groups/structures` | [List structure resolvers](/documentation/resource-reference/collaborator-groups/structures) | ## Engagements | Method | Path | Description | |--------|------|-------------| | GET | `/engagements` | [List engagements](/documentation/resource-reference/engagements/list) | | GET | `/engagements/:id` | [Get engagement](/documentation/resource-reference/engagements/get-by-id) | | GET | `/engagements/engagement-types` | [Get engagement types](/documentation/resource-reference/engagements/engagement-types) | ## Conversions | Method | Path | Description | |--------|------|-------------| | GET | `/conversions` | [List conversions](/documentation/resource-reference/conversions/list) | | GET | `/conversions/:id` | [Get conversion](/documentation/resource-reference/conversions/get-by-id) | | POST | `/conversions` | [Create conversion](/documentation/resource-reference/conversions/create) | | PATCH | `/conversions/:id` | [Update conversion](/documentation/resource-reference/conversions/update) | | DELETE | `/conversions/:id` | [Delete conversion](/documentation/resource-reference/conversions/delete) | | GET | `/conversions/conversion-types` | [Get conversion types](/documentation/resource-reference/conversions/conversion-types) | | POST | `/conversions/bulk` | [Bulk actions](/documentation/resource-reference/conversions/bulk-actions) | ## Transactions | Method | Path | Description | |--------|------|-------------| | GET | `/transactions` | [List transactions](/documentation/resource-reference/transactions/list) | | GET | `/transactions/:id` | [Get transaction](/documentation/resource-reference/transactions/get-by-id) | | POST | `/transactions` | [Create transaction](/documentation/resource-reference/transactions/create) | | POST | `/transactions/credit-collaborator` | [Credit collaborator](/documentation/resource-reference/transactions/credit-collaborator) | | GET | `/transactions/sources` | [Get transaction sources](/documentation/resource-reference/transactions/sources) | | POST | `/transactions/bulk` | [Bulk actions](/documentation/resource-reference/transactions/bulk-actions) | ## Obligations | Method | Path | Description | |--------|------|-------------| | GET | `/obligations` | [List obligations](/documentation/resource-reference/obligations/list) | | GET | `/obligations/:id` | [Get obligation](/documentation/resource-reference/obligations/get-by-id) | | POST | `/obligations` | [Create obligation](/documentation/resource-reference/obligations/create) | | PATCH | `/obligations/:id` | [Update obligation](/documentation/resource-reference/obligations/update) | | DELETE | `/obligations/:id` | [Delete obligation](/documentation/resource-reference/obligations/delete) | | POST | `/obligations/bulk` | [Bulk actions](/documentation/resource-reference/obligations/bulk-actions) | ## Fulfillments | Method | Path | Description | |--------|------|-------------| | GET | `/fulfillments` | [List fulfillments](/documentation/resource-reference/fulfillments/list) | | GET | `/fulfillments/:id` | [Get fulfillment](/documentation/resource-reference/fulfillments/get-by-id) | | POST | `/fulfillments/generate` | [Generate fulfillments](/documentation/resource-reference/fulfillments/generate) | | PATCH | `/fulfillments/:id` | [Update fulfillment](/documentation/resource-reference/fulfillments/update) | | DELETE | `/fulfillments/:id` | [Delete fulfillment](/documentation/resource-reference/fulfillments/delete) | | POST | `/fulfillments/bulk` | [Bulk actions](/documentation/resource-reference/fulfillments/bulk-actions) | ## Payouts | Method | Path | Description | |--------|------|-------------| | GET | `/payouts` | [List payouts](/documentation/resource-reference/payouts/list) | | GET | `/payouts/:id` | [Get payout](/documentation/resource-reference/payouts/get-by-id) | | POST | `/payouts/export` | [Export payouts](/documentation/resource-reference/payouts/export) | | POST | `/payouts/bulk` | [Bulk actions](/documentation/resource-reference/payouts/bulk-actions) | ## Distributors | Method | Path | Description | |--------|------|-------------| | GET | `/distributors` | [List distributors](/documentation/resource-reference/distributors/list) | | GET | `/distributors/:id` | [Get distributor](/documentation/resource-reference/distributors/get-by-id) | | POST | `/distributors` | [Create distributor](/documentation/resource-reference/distributors/create) | | PATCH | `/distributors/:id` | [Update distributor](/documentation/resource-reference/distributors/update) | | DELETE | `/distributors/:id` | [Delete distributor](/documentation/resource-reference/distributors/delete) | | POST | `/distributors/bulk` | [Bulk actions](/documentation/resource-reference/distributors/bulk-actions) | ## Distributions | Method | Path | Description | |--------|------|-------------| | GET | `/distributions` | [List distributions](/documentation/resource-reference/distributions/list) | | GET | `/distributions/:id` | [Get distribution](/documentation/resource-reference/distributions/get-by-id) | | POST | `/distributions/credit-metric` | [Credit metric](/documentation/resource-reference/distributions/credit-metric) | ## Notes | Method | Path | Description | |--------|------|-------------| | GET | `/notes` | [List notes for a source](/documentation/resource-reference/notes/list) | | GET | `/notes/:id/position` | [Get note position in feed](/documentation/resource-reference/notes/position) | ## Reporting | Method | Path | Description | |--------|------|-------------| | GET | `/reporting` | [Query reporting data](/documentation/resource-reference/reporting) | ## Events | Method | Path | Description | |--------|------|-------------| | POST | `/event/site-visited` | [Report a site visit](/documentation/resource-reference/events/site-visited) | | POST | `/event/sale` | [Report a sale](/documentation/resource-reference/events/sale) | | POST | `/event/refund` | [Report a refund](/documentation/resource-reference/events/refund) | ## AllocationCompleted Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-distributions/allocation-completed Fires when an individual collaborator's share of a distribution has been calculated and recorded. # AllocationCompleted `AllocationCompleted` fires when an individual collaborator's share of a distribution has been calculated and recorded. This is the bridge between the distribution system and the standard payment pipeline. The allocation's value becomes the obligation amount, so from this point forward the flow is identical to what happens after a program-based conversion. The event ID is `allocation_completed`, and its fully qualified class is `Siren\Distributions\Core\Events\AllocationCompleted`. ## What does this event carry? The event carries the `Allocation` model, which records which collaborator received the allocation, how much they were allocated, and which distribution it came from. ```php use Siren\Distributions\Core\Events\AllocationCompleted; public function handle(Event $event): void { $allocation = $event->getAllocation(); // The allocation model links a specific collaborator // to their calculated share of the distribution's reward pool } ``` ## Where does the allocation go next? The allocation value enters the obligation pipeline just as a conversion-based reward would. An obligation is created for the collaborator at the allocated amount, and from there it follows the standard approval and fulfillment workflow: pending, approved, and eventually paid out. This design means the fulfillment system does not need to distinguish between program-based rewards and distribution-based rewards. Both arrive as obligations with an amount and a collaborator, and both are processed the same way during payout generation. For the full lifecycle of distribution events and how they connect to the fulfillment pipeline, see the [Distribution Events overview](/documentation/developer-reference/events-distributions). ## Attribution Events Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-attribution Domain events for opportunity tracking and engagement creation. # Attribution Events Attribution events handle the middle of Siren's pipeline. They sit between the [commerce events](/documentation/developer-reference/events-commerce) that enter the system from e-commerce platforms and the conversion events that generate obligations and payouts. This is where Siren determines which collaborator gets credit for a customer action, creates the engagement records that represent that credit, and signals the conversion system to proceed. All opportunity events live in `Siren\Opportunities\Core\Events`. All engagement events live in `Siren\Engagements\Core\Events`. ## Opportunity lifecycle Opportunities are temporary tracking records that capture a customer's referral context before a conversion occurs. Two events manage their lifecycle. [OpportunityTriggered](/documentation/developer-reference/events-attribution/opportunity-triggered) fires when a new opportunity is created, such as when a customer visits a site through an affiliate link or uses a coupon. Engagement trigger strategies evaluate this event to decide whether to create engagements. [OpportunityInvalidated](/documentation/developer-reference/events-attribution/opportunity-invalidated) fires when an existing opportunity is determined to be invalid, whether from duplicate detection, expired referral links, or other validation failures. Any engagements that were created for the opportunity are marked invalid, preventing them from contributing to conversions. ## From opportunity to engagement Three events manage the process of creating, awarding, and completing engagements. They represent a progression: engagements are triggered (created in bulk), individually awarded (given credit within a program), and then completed (marked as having led to a conversion). [EngagementsTriggered](/documentation/developer-reference/events-attribution/engagements-triggered) fires when engagement trigger strategies produce new engagement records. A single opportunity can produce multiple engagements if the collaborator participates in multiple programs, so this event always carries an array rather than a single engagement. [EngagementAwarded](/documentation/developer-reference/events-attribution/engagement-awarded) fires when an individual engagement is awarded credit during the conversion process. This is different from triggering. Triggering creates the engagement record. Awarding happens later, when the conversion system evaluates which engagements should receive credit based on the program's resolver strategy. [EngagementCompleted](/documentation/developer-reference/events-attribution/engagement-completed) fires when engagements for an opportunity finish the conversion process. This signals that the opportunity's engagements have been fully processed and should transition from active to completed status. ## Bridging into the conversion system Two internal coordination events handle the transition from the attribution layer into the conversion system. `EngagementInitialized` (event ID: `Engagement_initialized`) fires when the engagement system begins processing a sale or lead for conversion. It carries an opportunity ID, line item details from the commerce event, and the engagement type. This event signals the conversion system to prepare for building conversion records. `TransactionTriggered` (event ID: `transaction_triggered`) fires when the attribution system produces the data needed for a transaction record. It carries an opportunity ID and line item details. When this event fires, `InitializeSaleEngagement` picks it up and begins evaluating which engagement trigger strategies apply. This is the handoff point where financial data from the commerce layer flows into the engagement system's attribution logic. These two events are internal plumbing. Most integrations will listen to the five events documented on their individual pages rather than these coordination signals. ## The flow from opportunity through engagement to conversion The attribution events form a clear path through the pipeline. An opportunity is created when a customer interacts with a referral link, and `OpportunityTriggered` fires. Engagement trigger strategies evaluate the opportunity and create engagement records, producing `EngagementsTriggered`. When a sale or lead comes in, `TransactionTriggered` and `EngagementInitialized` carry the commerce data into the engagement system. The conversion system awards credit to the relevant engagements, producing `EngagementAwarded` for each one. Finally, `EngagementCompleted` signals that all engagements for the opportunity have been processed and the conversion is done. If an opportunity is determined to be invalid at any point, `OpportunityInvalidated` fires and the associated engagements are invalidated before they can contribute to conversions. The conversion events that follow this flow (such as `ConversionsAwarded` and `ObligationIssued`) are documented separately. They belong to the conversion and obligation domains rather than the attribution layer. ## Beacon MCP Server Source: https://www.sirenaffiliates.com/documentation/beacon/introduction Connect Siren's AI assistant to your development tools via MCP. Available tools, server URL, and setup guide links. Beacon is Siren's MCP (Model Context Protocol) server. It gives AI assistants direct access to Siren's complete knowledge base and the ability to generate installable program configurations called recipes. Beacon is free for everyone, no Siren license required. If you just want a quick way to chat with Beacon, the [Siren Affiliates Beacon custom GPT](https://chatgpt.com/g/g-69d6578e07bc8191b5e0820b489f6446-siren-affiliates-beacon) on ChatGPT works immediately with no setup. The MCP integration described here is for connecting Beacon to development tools and AI workflows where you want programmatic access to its capabilities. ## Server URL All setup methods use the same URL: ``` https://beacon.sirenaffiliates.com/beacon/v1/mcp ``` ## Available tools After connecting, your AI assistant has access to three tools. ### beacon_search_knowledge Search Siren's complete knowledge base including documentation, blog posts, recipes, podcast transcripts, and guides. You can filter results by content type: `documentation`, `blog`, `recipe`, `podcast`, `feature`, or `integration`. ### beacon_fetch_knowledge Read the full content of any knowledge article. Use this after searching to get complete details on a specific topic. ### beacon_create_recipe Create a custom Siren recipe: a fully-resolved program configuration. A recipe can include programs, program groups, and distributors. Once generated, the recipe can be applied to any Siren installation to set everything up in one operation. ## Setup guides Connect Beacon to your preferred platform: - [Claude.ai and Claude Code](/documentation/beacon/claude) - [ChatGPT (MCP integration)](/documentation/beacon/chatgpt) - [Code editors (VS Code, JetBrains, Cursor, etc.)](/documentation/beacon/code-editors) ## Example conversations Here are some things you can ask your AI assistant after connecting Beacon: - "I want to create a tiered affiliate program where top performers earn higher commissions. What's the best way to set this up in Siren?" - "Search for recipes related to course creator royalty programs" - "I need a program that pays affiliates for referral links AND coupon codes, with the most recent engagement getting credit. Can you create a recipe for that?" - "What integrations does Siren support? I'm using WooCommerce with Subscriptions." ## Blog Post Visited Source: https://www.sirenaffiliates.com/documentation/general/blog-post-visited When a visitor reads a blog post authored by a collaborator, the Blog Post Visited event is triggered. import EventFlow from "@/components/content/EventFlow.astro"; When a visitor reads a blog post authored by a [collaborator](/documentation/general/what-is-a-collaborator), the "Blog Post Visited" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the pageview to the collaborator who wrote the post. ## How it's typically used Blog Post Visited is built for sites that publish content written by outside contributors and want to pay those contributors based on how much traffic their writing actually pulls in. You handle the platform and the editorial process, and the writer gets credited every time someone reads their post, no matter how the reader got there. This trigger pairs naturally with a traffic-driving program like a [basic affiliate program](/recipes/basic-affiliate-program). Affiliates drive visitors to the site, those visitors read posts written by contributors, and both sides get credit through their own [programs](/documentation/general/what-are-programs). The [blogger revenue program](/recipes/blogger-revenue-program) recipe is a ready-to-use setup for paying contributors this way. Posts are attributed through their WordPress author field, so any post assigned to a collaborator's user account will fire the trigger when it's viewed. ## Bridging Platform Hooks Source: https://www.sirenaffiliates.com/documentation/extensions/wp-bridging-hooks How event bindings connect WordPress plugin hooks to Siren domain events, the mechanism that makes Siren multi-platform. import CodeTabs from "@/components/content/CodeTabs.astro"; # Bridging Platform Hooks When WooCommerce fires `woocommerce_new_order`, Siren needs to translate that into a `SaleTriggered` domain event. When Easy Digital Downloads fires `edd_complete_purchase`, the same thing needs to happen. [Event bindings](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding) declare these mappings, and transformer callables do the translation. This is the mechanism that makes Siren platform-agnostic. The core never sees WordPress hooks, only typed domain events. ## How does the bridge work? Each integration declares event bindings in `getEventBindings()`. A binding says: "When this WordPress action fires, call this transformer. If the transformer returns an event, broadcast it." The transformer receives the hook's raw arguments and either produces a domain event or returns `null` to skip. The key insight is that different platforms fire different hooks with different data shapes, but they all produce the same domain events. Siren's core processes `SaleTriggered` identically whether it originated from WooCommerce, Easy Digital Downloads, or LifterLMS. ```php // WooCommerce fires woocommerce_new_order with an order ID return [ SaleTriggered::class => [ ['action' => 'woocommerce_new_order', 'transformer' => function ($orderId) { $order = wc_get_order($orderId); if (!$order) return null; // Find the affiliate opportunity for this customer $opportunity = $this->locateOpportunity($order->get_user_id()); if (!$opportunity) return null; // Check if this order was already processed if ($this->alreadyTracked($orderId, 'wc_order')) return null; // Convert WooCommerce order data into a Siren event $details = $this->buildTransactionDetails($order); return new SaleTriggered($opportunity->getId(), $details, 'wc', $orderId, 'wc_order'); }], ], ]; ``` ```php // EDD fires edd_complete_purchase with a payment ID return [ SaleTriggered::class => [ ['action' => 'edd_complete_purchase', 'transformer' => function ($paymentId) { $payment = edd_get_payment($paymentId); if (!$payment) return null; // Find the affiliate opportunity for this customer $opportunity = $this->locateOpportunity($payment->user_id); if (!$opportunity) return null; // Check if this payment was already processed if ($this->alreadyTracked($paymentId, 'edd_order')) return null; // Convert EDD payment data into a Siren event $details = $this->buildTransactionDetails($payment); return new SaleTriggered($opportunity->getId(), $details, 'edd', $paymentId, 'edd_order'); }], ], ]; ``` ```php // LifterLMS fires lifterlms_order_complete with an order object return [ SaleTriggered::class => [ ['action' => 'lifterlms_order_complete', 'transformer' => function ($order) { $userId = $order->get('user_id'); if (!$userId) return null; // Find the affiliate opportunity for this student $opportunity = $this->locateOpportunity($userId); if (!$opportunity) return null; // Check if this order was already processed if ($this->alreadyTracked($order->get('id'), 'llms_order')) return null; // Convert LifterLMS order data into a Siren event $details = $this->buildTransactionDetails($order); return new SaleTriggered($opportunity->getId(), $details, 'llms', $order->get('id'), 'llms_order'); }], ], ]; ``` Three different hooks, three different data shapes, one domain event. Siren's attribution logic, conversion tracking, and payout calculations work identically regardless of which integration produced the event. ## What does the binding format look like? The full format returned by `getEventBindings()` maps domain event classes to arrays of hook/transformer pairs: ```php public function getEventBindings(): array { return [ SaleTriggered::class => [ ['action' => 'platform_order_hook', 'transformer' => $saleCallback], ], TransactionCompleted::class => [ ['action' => 'platform_payment_confirmed', 'transformer' => $completedCallback], ], RefundTriggered::class => [ // Multiple hooks can map to the same event ['action' => 'platform_order_refunded', 'transformer' => $refundCallback], ['action' => 'platform_order_cancelled', 'transformer' => $refundCallback], ], ]; } ``` You can bind multiple WordPress hooks to the same domain event. This is common for refunds, where an order can be reversed through several different status transitions. ## When does the transformer return null? Returning `null` from a transformer tells the framework to silently skip the event. No error, no log entry. The hook fired but there was nothing for Siren to do. Common reasons to return null: - The customer was not referred by an affiliate, so there is no engagement to track. - The mapping table shows this order has already been processed. This prevents double-counting when a hook fires more than once. - The order has no line items, the payment amount is zero, or required fields are missing. - The hook fired for an order type your extension does not handle (e.g., a manual adjustment, not a real sale). This convention keeps the core clean. The framework never needs to handle "no-op" cases. Transformers handle that filtering at the boundary. ## Where to go next For the full transformer and event binding reference (constructor signatures, the adapter pattern, and all commerce events), see [Event Bindings and Transformers](/documentation/extensions/event-bindings-and-transformers) and [Commerce Events](/documentation/extensions/commerce-events). For how to listen to events after they are broadcast, see [Hooks, Actions & Events](/documentation/extensions/wp-hooks-and-events). For the full PHPNomad framework documentation on event binding, see [Event Binding](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding). ## Bulk Actions: Collaborators Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/bulk-actions Perform actions on multiple collaborators at once, including status changes, program enrollment, and signup emails. ### Bulk Actions `POST /siren/v1/collaborators/bulk` Performs an action on multiple collaborators at once. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `activate`, `deactivate`, `restore`, `delete`, `permanent_delete`, `addToProgram`, `removeFromProgram`, `sendSignupEmail` | | `ids` | integer[] | Yes | Array of collaborator IDs to act upon | | `programId` | integer | Conditional | Required when action is `addToProgram` or `removeFromProgram` | **Action Behaviors:** | Action | Effect | |---|---| | `activate` | Sets status to `active` | | `deactivate` | Sets status to `inactive` | | `restore` | Sets status to `inactive` (restores from trash for review) | | `delete` | Sets status to `deleted` (soft delete) | | `permanent_delete` | Permanently removes records from the database | | `addToProgram` | Enrolls collaborators in the specified program | | `removeFromProgram` | Removes collaborators from the specified program | | `sendSignupEmail` | Sends the signup/registration email to each collaborator (broadcasts `CollaboratorAccountReady` per record) | Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing of remaining IDs. **Example Request:** ```json { "action": "addToProgram", "ids": [7, 12, 19], "programId": 3 } ``` **Example Response:** ```json { "success": true, "affected": 3 } ``` ## Bulk Actions: Conversions Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/bulk-actions Perform actions on multiple conversions at once, including approval, rejection, and deletion. ### Bulk Actions `POST /siren/v1/conversions/bulk` Performs an action on multiple conversions at once. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `approve`, `reject`, `markPending`, `delete`, `permanent_delete` | | `ids` | integer[] | Yes | Array of conversion IDs to act upon | #### Action Behaviors | Action | Effect | Events | |---|---|---| | `approve` | Sets status to `approved` via `ConversionApproveService` | Broadcasts `ConversionApproved` per record, which triggers `MarkObligationsAsPending` to transition the linked obligation from `draft` to `pending` | | `reject` | Sets status to `rejected` | Broadcasts `ConversionRejected` per record, which triggers `RejectObligations` to transition the linked obligation from `draft` to `rejected` | | `markPending` | Sets status to `pending` | -- | | `delete` | Sets status to `deleted` (soft delete) | -- | | `permanent_delete` | Permanently removes records from database | -- | Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing of remaining IDs. #### Example Request ```json { "action": "approve", "ids": [1, 2, 3] } ``` #### Example Response ```json { "success": true, "affected": 3 } ``` ## Bulk Actions: Distributors Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors/bulk-actions Performs an action on multiple distributors at once, including activate, deactivate, restore, delete, and permanent delete. # Bulk Actions `POST /siren/v1/distributors/bulk` Performs an action on multiple distributors at once. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `activate`, `deactivate`, `restore`, `delete`, `permanent_delete` | | `ids` | integer[] | Yes | Array of distributor IDs to act upon | **Action Behaviors:** | Action | Effect | Auth Required | |---|---|---| | `activate` | Sets status to `active` | Update | | `deactivate` | Sets status to `inactive` | Update | | `restore` | Sets status to `inactive` (restores from trash for review) | Update | | `delete` | Sets status to `deleted` (soft delete) | Delete | | `permanent_delete` | Permanently removes records from database | Delete | Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing of remaining IDs. **Example Request:** ```json { "action": "activate", "ids": [1, 2, 3] } ``` **Example Response:** ```json { "success": true, "affected": 3 } ``` ## Bulk Actions: Obligations Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations/bulk-actions Perform an action on multiple obligations at once, including approve, reject, and delete. ### Bulk Actions `POST /siren/v1/obligations/bulk` Performs an action on multiple obligations at once. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `approve`, `reject`, `markPending`, `markComplete`, `delete`, `permanent_delete` | | `ids` | integer[] | Yes | Array of obligation IDs to act upon | #### Action Behaviors | Action | Status Set | Auth Required | |---|---|---| | `approve` | `approved` | Update | | `reject` | `rejected` | Update | | `markPending` | `pending` | Update | | `markComplete` | `complete` | Update | | `delete` | `deleted` (soft delete) | Delete | | `permanent_delete` | (record removed) | Delete | Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing. #### Example Request ```json { "action": "markComplete", "ids": [10, 11, 12] } ``` #### Example Response ```json { "success": true, "affected": 3 } ``` ### Bulk Action Statuses The bulk action endpoint supports additional status values (`approved`, `complete`, `deleted`) that map to the broader set of internal states used by the system for workflow management. ## Bulk Actions: Program Groups Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups/bulk-actions Performs an action on multiple program groups at once. Only permanent delete is supported. # Bulk Actions `POST /siren/v1/program-groups/bulk` Performs an action on multiple program groups at once. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | The action to perform. Only `delete` is supported. | | `ids` | integer[] | Yes | Array of program group IDs to act upon | Because program groups have no status field, `delete` is always a permanent removal. Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing of remaining IDs. **Example Request:** ```json { "action": "delete", "ids": [1, 2, 3] } ``` **Example Response:** ```json { "success": true, "affected": 3 } ``` ## Bulk Actions: Programs Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/bulk-actions Performs an action on multiple programs at once, including activate, deactivate, restore, delete, and permanent delete. # Bulk Actions `POST /siren/v1/programs/bulk` Performs an action on multiple programs at once. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `activate`, `deactivate`, `restore`, `delete`, `permanent_delete` | | `ids` | integer[] | Yes | Array of program IDs to act upon | **Action Behaviors:** | Action | Effect | |---|---| | `activate` | Sets status to `active` | | `deactivate` | Sets status to `inactive` | | `restore` | Sets status to `inactive` (restores from trash for review) | | `delete` | Sets status to `deleted` (soft delete) | | `permanent_delete` | Permanently removes records from the database | Authorization is dynamic: `activate`, `deactivate`, and `restore` require Update permission, while `delete` and `permanent_delete` require Delete permission. Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing of remaining IDs. **Example Request:** ```json { "action": "activate", "ids": [1, 2, 3] } ``` **Example Response:** ```json { "success": true, "affected": 3 } ``` **Events:** Broadcasts `ProgramActionEvent` with the appropriate action type after success. ## Bulk Actions: Transactions Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions/bulk-actions Perform an action on multiple transactions at once, including cancel, refund, and delete. ### Bulk Actions `POST /siren/v1/transactions/bulk` Performs an action on multiple transactions at once. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `cancel`, `refund`, `delete` | | `ids` | integer[] | Yes | Array of transaction IDs to act upon | **Action Behaviors:** | Action | Effect | |---|---| | `cancel` | Sets status to `cancelled` | | `refund` | Sets status to `refunded` | | `delete` | Permanently removes records from the database | Non-existent IDs are silently skipped. Datastore errors on individual records are logged but do not halt processing of remaining IDs. **Example Request:** ```json { "action": "cancel", "ids": [101, 102, 103] } ``` **Example Response:** ```json { "success": true, "affected": 3 } ``` ## Choosing a calculation strategy Source: https://www.sirenaffiliates.com/documentation/calculation-strategies/choosing-a-calculation-strategy How to pick between Fixed and the cascade family when configuring an engagement type or metric type. When you enable an engagement type on a [program](/documentation/general/what-are-programs), or a metric type on a [distributor](/documentation/general/what-are-distributors), Siren asks how to calculate the score for each trigger. That choice is the calculation strategy, and the question underneath it is simple: when a single trigger fires, who should it pay? The answer might be one person, the chain of people above them, or the chain of people below them. Those three answers map to the three strategies, and they are what the rest of this page works through. Fixed answers the question with one recipient. It emits a single score for the collaborator who actually did the work, which is what most programs and most distributors want. It needs no [collaborator group](/documentation/general/what-are-collaborator-groups) at all, and if one is bound it works regardless of how that group is structured. If you have never thought about spreading a score across more than one person, [Fixed](/documentation/calculation-strategies/fixed) is the strategy to pick. Once you want a trigger to pay more than one person, the direction of the payout decides the strategy, and both cascade options walk the triggering collaborator's chain up to five layers. [Upline Cascade](/documentation/calculation-strategies/upline-cascade) walks toward the top of the chain, emitting a score for each collaborator above the trigger. Layer 1 is the direct upline, layer 2 is their upline, and so on. Reach for it when people above someone should earn from the work that person does, such as management overrides or team-lead bonuses. [Downline Cascade](/documentation/calculation-strategies/downline-cascade) walks the other direction, emitting a score for each collaborator below the trigger so the people someone manages or leads each earn a share when the trigger fires. Layer 1 is the direct downline, layer 2 is their downline, and so on. It fits top-down rollups where a sale by a manager rewards the team underneath them, or team-goal incentives where a manager hitting a target pushes a bonus down to the reps they lead. Both cascades require a [linear chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) group bound to the program or distributor, and the picker hides them when the bound collaborator group is flat or unbound. See [Why some calculation methods disappear](/documentation/general/calc-capability-matching) for the reasoning, and read [What is a cascade](/documentation/general/what-is-a-cascade) first if the idea of layers and walking a chain is new. The picker behaves the same way on the Programs Edit and Distributors Edit screens, so picking Upline on an engagement type and on a metric type means the same thing, walking upline from the trigger and crediting each layer. The two sides read the bound group and the per-layer args from different config locations: an engagement type reads from the program config, and a metric type reads from the distributor config. Each cascade page covers the exact config types in its Configuration section. The picker is the only guard against an unworkable cascade, and it acts at edit time. If a cascade is saved and the bound group is later flattened, deleted, or unbound, the calc keeps the cascade strategy and pays out nothing rather than reverting to Fixed. See [Cascade troubleshooting](/documentation/calculation-strategies/cascade-troubleshooting) for the states that produce zero payouts after configuration and how to recover. ## Choosing a collaborator group structure Source: https://www.sirenaffiliates.com/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure How to pick between flat, linear chain, and parent-child structures for a collaborator group. The structure on a [collaborator group](/documentation/general/what-are-collaborator-groups) controls how a [cascade](/documentation/general/what-is-a-cascade) reads the group. The right pick follows the shape of the people in the group. Some groups have no shape at all, just a set of members. Some run in a single line, one position after another. Some branch into a tree. Match the structure to the shape your collaborators are already in and the cascade will read it the way you expect. Start at the simplest end of that spectrum. When the collaborators in a group have no order and no hierarchy, [flat](/documentation/collaborator-group-structures/flat) is the structure you want. Members sit in a plain unordered list, and the cascade calcs disappear from the picker because there are no layers to walk. Flat is the default and the right choice for most programs, where you only need to know who belongs to the group rather than how they rank against each other. Pair it with a fixed calc and every member earns the same amount when they trigger an engagement. Add order to that group and you move to the next shape along the spectrum. A [linear chain](/documentation/collaborator-group-structures/linear-chain) arranges collaborators into an ordered sequence where each position has at most one upline and one downline, so it fits a ranked sales team or a reporting line where each person sits under one manager, person A above B, B above C, and so on. When someone triggers an engagement, the cascade walks up or down the chain one position at a time and credits each layer at the rate you have configured. A linear chain requires Siren Pro. Let that single line branch and you reach the most structured shape. A [parent-child](/documentation/collaborator-group-structures/parent-child) group is a tree where one parent can have many children and each child can have children of its own, which is what an org chart or a brokerage looks like once a manager owns several teams that each have their own lead. The cascade walks the tree outward from the triggering collaborator, upline toward the root and downline toward the leaves, crediting each layer along the way. Like the linear chain, parent-child requires Siren Pro. ## Changing a group's structure later You can switch an existing group's structure at any time, and its members carry over. The structural metadata that orders those members does not. Switching to linear chain leaves every member without a `position`, which the resolver reads as position 0 (topmost), so the chain has no real order until you set positions again. Switching to parent-child leaves every member without a parent. Switching back to flat is harmless because flat ignores per-member metadata entirely. After any structure change, re-establish the positions or parent relationships the new structure needs before you rely on a cascade. For step-by-step guidance on converting a tiered program-group setup into a single program bound to a cascade, see [migrate a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade). For moving an existing install onto the Plus and Pro tiers that collaborator groups and cascades require, see [upgrading to Plus and Pro](/documentation/migration/upgrading-to-plus-and-pro). ## Choosing a Distribution Structure Source: https://www.sirenaffiliates.com/documentation/distribution-structures/choosing-a-distribution-structure How to pick between Shared Engagement Pool, Performance Weighted Pool, and Top Score Wins for your distributor. A distribution structure decides how a [distributor's](/documentation/general/what-are-distributors) reward pool gets divided among collaborators when the distribution triggers. Where a program structure handles a single conversion, a distribution structure handles everything that happened over a tracking period. You'll face this choice when you're configuring a distributor and Siren asks how the accumulated pool should be split. Performance Weighted Pool is the most common choice and the one you'll want unless you have a specific reason to pick something else. It rewards proportionally without creating a winner-take-all dynamic, which is what most ongoing revenue-share arrangements actually want. All three structures require Siren Essentials. All three also work with a [cascade-bound distributor](/documentation/general/what-is-a-cascade). When the distributor is bound to a collaborator group with an Upline or Downline cascade calc, the cascade credits upline or downline peers with per-layer scores, and those scores feed the structure exactly like direct scores do. The difference is in how each structure then uses them: the Shared Engagement Pool treats any non-zero cascade score as eligibility for an equal share, the Performance Weighted Pool divides proportionally over the combined score, and Top Score Wins can hand the whole pool to a peer whose cascade points out-score everyone else. Pick the structure first on its own merits, then size the per-layer values on the [distributor compensation modeling](/documentation/distribution-structures/distributor-compensation-modeling) page. ## Quick comparison | Structure | What it does | Best for | |---|---|---| | Shared Engagement Pool | Splits the pool equally among everyone who earned any score | Flat participation stipends | | Performance Weighted Pool | Splits the pool proportionally by metric score | Ongoing revenue-share programs | | Top Score Wins | Gives the full pool to the single top scorer | Monthly bonus competitions | ## How to choose ### Use Performance Weighted Pool if... You're running an ongoing revenue-share program and you want every active collaborator paid in proportion to what they contributed. This is the right pick for content creator profit shares, instructor revenue splits, and quarterly team bonuses tied to performance. It rewards heavier contributors more without shutting anyone out, which keeps the incentive honest for both your top performers and everyone still ramping up. ### Use Top Score Wins if... You want a competition with a single winner per period. Monthly sales bonuses, quarterly leaderboard prizes, and milestone rewards where one collaborator takes the whole pool all fit here. It works best when your top tier of collaborators can realistically compete with each other and when your tracked metrics are hard to game. Avoid it when the same person would win every period, because the incentive collapses for everyone else. ### Use Shared Engagement Pool if... You want a flat stipend split evenly among everyone who participated, regardless of how much they contributed. This fits early-stage creator platforms where you want broad participation before you have enough data to reward performance, or flat-fee contributor programs where showing up is what you're paying for. It's the simplest structure to explain, and the simplest to run, but it won't motivate your top performers to push harder. ## See the full details - [Performance Weighted Pool](/documentation/distribution-structures/performance-weighted-pool) for proportional splits by metric score. - [Top Score Wins](/documentation/distribution-structures/top-score-wins) for single-winner competitions. - [Shared Engagement Pool](/documentation/distribution-structures/shared-engagement-pool) for equal-share participation pools. ## Further reading - [Distributor compensation modeling](/documentation/distribution-structures/distributor-compensation-modeling) walks through how to size the pool and assign metric weights once you've picked a structure. - [Moving creators from flat-rate to performance-based pay](/blog/moving-creators-from-flat-rate-to-performance-based-pay) covers the strategic side of switching an existing flat-rate creator program over to a distributor. ## Choosing a Program Group Structure Source: https://www.sirenaffiliates.com/documentation/program-group-structures/choosing-a-program-group-structure How to pick between Newest Engagement Wins and Oldest Engagement Wins for your program group. A program group structure decides which [program](/documentation/general/what-are-programs) inside a [program group](/documentation/general/what-are-program-groups) actually runs when a customer converts. Only one program per group can fire per conversion, and this setting tells Siren how to pick the winner when more than one could apply. You'll face this choice when you're configuring a program group and Siren asks how it should resolve ties. For most affiliate setups, Newest Engagement Wins is the right answer and the one you'll see in almost every recipe. Oldest Engagement Wins is the exception, reserved for programs where early lead generation is what you actually want to reward. A program group structure is a different setting from a [collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure). The names are similar, but they control different things. A program group structure decides which program wins a single conversion. A collaborator group structure (flat, linear-chain, or parent-child) decides how Siren walks a roster of collaborators to credit peers across layers when a cascade calculation runs. If you are trying to pay an upline or a team hierarchy rather than pick one winning program, see [what are collaborator groups](/documentation/general/what-are-collaborator-groups). ## Quick comparison | Structure | What it does | Best for | |---|---|---| | Newest Engagement Wins | Picks the program tied to the most recent engagement | Standard affiliate attribution | | Oldest Engagement Wins | Picks the program tied to the earliest engagement | Lead-generation programs | ## How to choose ### Use Newest Engagement Wins if... You're running a standard affiliate program and you want the collaborator who most recently influenced the customer to get paid. This is the default for a reason. When a customer clicks an affiliate link and buys shortly after, the most recent engagement is almost always the one that drove the sale. If you're unsure which structure to pick, this is it. ### Use Oldest Engagement Wins if... You want to reward the collaborator who brought the customer in originally, not the one who closed the sale. This fits programs where lead generation is the valuable work, like high-ticket B2B sales, agency services, or courses with long consideration cycles. It also works well when you pair a lead-generation program with a separate closing program in the same group, because each program can credit a different collaborator for their part in the journey. ## See the full details - [Newest Engagement Wins](/documentation/program-group-structures/newest-engagement-wins) for last-touch attribution. - [Oldest Engagement Wins](/documentation/program-group-structures/oldest-engagement-wins) for first-touch attribution. ## Choosing a Program Structure Source: https://www.sirenaffiliates.com/documentation/program-structures/choosing-a-program-structure How to pick between Shared Engagement Pool, Performance Weighted Pool, and Top Score Wins for your program. A program structure decides what happens when a customer converts and more than one collaborator has engaged with that customer. It's the rule Siren uses to split (or not split) a single conversion reward. You'll face this choice when you're configuring a [program](/documentation/general/what-are-programs) and the "structure" dropdown asks how credit should be divided. Most simple affiliate programs don't actually need this decision. If you only want the last collaborator who engaged the customer to be paid, that's handled at the [program group](/documentation/general/what-are-program-groups) level through Newest Engagement Wins, and you don't need a pool structure at all. The three structures on this page exist for programs that want to reward multiple contributors on a single sale. All three require Siren Essentials. The engagement scores these structures split on can come from a [cascade calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy) as well as from direct engagement. If the program is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, the cascade credits upline or downline group members per layer, and those scores feed whichever structure you pick the same way direct scores do. Cascade calcs require Siren Pro. ## Quick comparison | Structure | What it does | Best for | |---|---|---| | Shared Engagement Pool | Splits the reward equally among every engaged collaborator | Flat multi-touch attribution | | Performance Weighted Pool | Splits the reward proportionally by engagement score | Weighted multi-touch attribution | | Top Score Wins | Gives the full reward to the single highest-scoring collaborator | One winner per conversion | ## How to choose ### Use Shared Engagement Pool if... You want everyone who touched the sale to get the same cut, regardless of how much they contributed. This fits high-ticket sales cycles where several collaborators (a content creator, a demo host, a closer) each play a role that's hard to rank, and you'd rather credit them equally than argue about weights. It also works when your commissions are large enough that an even split still pays each collaborator meaningfully. ### Use Performance Weighted Pool if... You want to credit every engaged collaborator but still pay the heavier contributors more. This is the right pick when your collaborators generate measurable engagement (views, webinar attendance, content interactions) and you want the split to reflect that. It avoids winner-take-all dynamics while keeping the incentive to actually contribute. ### Use Top Score Wins if... You want a single collaborator paid per conversion, and you want it to be the one with the highest engagement score rather than the newest or oldest engagement. This is useful when you can measure engagement quality reliably and you want to reward the collaborator whose work most influenced the buying decision. Be cautious if your engagement metrics are easy to game, because a winner-take-all structure amplifies that risk. ## See the full details - [Shared Engagement Pool](/documentation/program-structures/shared-engagement-pool) for an equal split among engaged collaborators. - [Performance Weighted Pool](/documentation/program-structures/performance-weighted-pool) for a proportional split by engagement score. - [Top Score Wins](/documentation/program-structures/top-score-wins) for a single winner per conversion. ## Choosing an Incentive Structure Source: https://www.sirenaffiliates.com/documentation/incentive-structures/choosing-an-incentive-structure How to pick between Percentage of Transaction, Fixed Per Transaction, and Fixed Per Product for your program. An incentive structure decides how much a collaborator earns when a conversion happens. It answers a single question: is the commission tied to the sale's revenue, to the fact that a sale happened at all, or to the specific products that were sold? You'll face this choice when you're configuring a [program](/documentation/general/what-are-programs) and Siren asks how the reward should be calculated. Percentage of Transaction is the default for almost every affiliate program, and if you're not sure what to pick, start there. The fixed-fee structures exist for specific situations where a percentage doesn't fit. Percentage of Transaction and Fixed Per Transaction are available in all tiers. Fixed Per Product requires Siren Essentials. ## Incentive structures versus calculation strategies Incentive structures and [calculation strategies](/documentation/calculation-strategies/choosing-a-calculation-strategy) are two different settings that both sound like they decide pay. They are separate axes. An incentive structure decides the shape of the reward for a single conversion: a percentage of revenue, a flat fee, or a per-product amount. A calculation strategy decides who gets scored and how a score fans out across a [collaborator group](/documentation/general/what-are-collaborator-groups). The default calculation strategy, Fixed, scores only the collaborator who triggered the conversion. The [Upline Cascade](/documentation/calculation-strategies/upline-cascade) and [Downline Cascade](/documentation/calculation-strategies/downline-cascade) strategies also score peers along a chain or tree. If you want a collaborator's upline or downline to earn from a sale, that is a calculation strategy choice, not an incentive structure. See [choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy). The two same-named "Fixed" and "Choosing" pages in that section describe scoring, not reward shape. ## Quick comparison | Structure | What it does | Best for | |---|---|---| | Percentage of Transaction | Pays a percentage of the sale total | Standard affiliate commissions | | Fixed Per Transaction | Pays a flat fee per completed sale | Lead generation and low-margin sales | | Fixed Per Product | Pays a flat fee per unit of a specific product | Uniform-priced catalogs and marketplaces | ## How to choose ### Use Percentage of Transaction if... You're running a normal affiliate program and you want commissions to scale with revenue. This is the right pick for most e-commerce stores, SaaS products, course platforms, and anything else where sale sizes vary and you want affiliates motivated to promote higher-value purchases. It's also the structure nearly every "basic affiliate program" recipe uses, so if your setup looks anything like a standard affiliate program, start here. ### Use Fixed Per Transaction if... The act of converting matters more to you than the transaction's revenue. This fits lead-generation programs where you pay a flat fee per qualified lead, referral programs where every new customer is worth roughly the same, and low-margin stores where a percentage would either eat your margin or be too small to motivate anyone. It's also useful when you run frequent discounts and don't want commissions shrinking along with the sale price. ### Use Fixed Per Product if... You're selling a catalog of uniformly-priced items, and you want to pay a set amount per unit sold rather than per transaction or per dollar. This is built for marketplaces, subscription services with a single price point, and catalogs where individual products have stable, predictable margins. A fitness store paying $10 per set of dumbbells sold is the canonical example. Skip this one if your prices vary widely, because the same flat fee across a $5 item and a $500 item rarely lines up with what you actually want to reward. ## See the full details - [Percentage of Transaction](/documentation/incentive-structures/percentage-of-transaction) for revenue-based commissions. - [Fixed Per Transaction](/documentation/incentive-structures/fixed-per-transaction) for flat fees per conversion. - [Fixed Per Product](/documentation/incentive-structures/fixed-per-product) for flat fees per unit sold. ## Further reading - [How much should you pay your affiliates](/blog/how-much-should-you-pay-affiliates) for picking the actual percentage or flat fee once you've picked a structure. ## Collaborator Group Events Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups Domain events for collaborator group lifecycle, membership, program and distributor binding, and resolver registration. # Collaborator Group Events Collaborator group events fire as operators build and wire up [collaborator groups](/documentation/general/what-are-collaborator-groups): the reusable rosters that programs and distributors bind to instead of listing individuals. They cover four areas: the group's own lifecycle, its membership, the bindings that connect a group to a program or distributor, and the registry-initiated events that extensions hook to register custom resolvers. All of them live under `Siren\Plus\Core\Groups\Events` (and, for structure resolution, `Siren\Plus\Core\Groups\Structure\Events`). ## Lifecycle events A group announces itself as it is created, edited, and removed. [CollaboratorGroupCreated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-created) fires when a new group is persisted, carrying the new group's id. [CollaboratorGroupRenamed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-renamed) and [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed) fire when an operator changes the name or switches the structure between flat, linear chain, and parent-child. [CollaboratorGroupDeleted](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted) fires as the group is removed, giving listeners a window to capture state before the row is gone. ## Membership events Membership changes fire their own events so the activity feed and any external mirror stay current. [CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) and [CollaboratorRemovedFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-removed-from-collaborator-group) fire as collaborators join or leave, and [CollaboratorGroupMemberMetadataChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed) fires when a member's structural metadata changes, such as a new position in a chain or a new parent in a tree. ## Binding events A group does nothing on its own until a program or distributor binds to it. [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group) and [ProgramUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group) fire when a program starts or stops paying through a group, and [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group) and [DistributorUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group) do the same on the distributor side. ## Registry-initiated events Three registry-initiated events are the extension seams for the group system. [CollaboratorGroupResolverRegistryInitiated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-resolver-registry-initiated) and [CollaboratorGroupMemberResolverRegistryInitiated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-resolver-registry-initiated) let extensions register field resolvers for groups and members, and [CollaboratorGroupStructureResolverRegistryInitiated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-resolver-registry-initiated) is the seam the Pro tier uses to register the linear chain and parent-child structure resolvers on top of the Plus flat default. To register a custom structure, see [collaborator group structures](/documentation/extensions/collaborator-group-structures). ## Collaborator Groups (REST) Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups REST endpoints, request/response shapes, and field reference for collaborator groups and members. A collaborator group is a named cluster of collaborators with an associated structure: flat, linear chain, or parent-child tree. Programs and distributors bind to a group so that calculation strategies can walk it and produce cascade payouts. For the operator-facing overview, see [What are collaborator groups](/documentation/general/what-are-collaborator-groups). This page is the REST reference: the wire shape of each resource, the endpoints that read and mutate them, and how a program or distributor gets bound to a group. ## Data shape ### CollaboratorGroup | Field | Type | Description | |---|---|---| | `id` | integer | Primary key. | | `name` | string | Display name. | | `description` | string | Free-form description. Empty string when not set. | | `structure` | string | Registered structure resolver id. Today's values: `flat`, `linearChain`, `parentChild`. See the [structure picker guide](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure). | The read endpoints (`GET /collaborator-groups` and `GET /collaborator-groups/{id}`) select fields through the resolver and expose only the four fields above. The record also carries `dateCreated` and `dateModified` timestamps. Those are not retrievable through the read endpoints, but the create response (`POST /collaborator-groups`) does return them. `linearChain` and `parentChild` require the Pro tier. Plus ships `flat` only, so a Plus-only install exposes `flat` as the only available `structure` value. ### CollaboratorGroupMember | Field | Type | Description | |---|---|---| | `id` | integer | Primary key. | | `groupId` | integer | The parent group's id. | | `collaboratorId` | integer | The collaborator's id. | | `metadata` | object | Structure-specific data. Always serialized as a JSON object, even when empty (`{}`). | | `dateCreated` | datetime | When the membership row was created. | | `dateModified` | datetime | When the membership row was last updated. | | `collaboratorName` | string | The collaborator's name, joined from the collaborator record. Read-only convenience field, so you can render a member without a second lookup. | | `collaboratorEmail` | string | The collaborator's email, joined from the collaborator record. Read-only convenience field. | | `collaboratorStatus` | string | The collaborator's status (for example `active`), joined from the collaborator record. Read-only convenience field. | The `metadata` object's keys depend on the parent group's `structure`: - **`flat`**: empty object. The flat resolver ignores all metadata. Every member is a peer. - **`linearChain`**: `{ "position": }`. Smaller positions sit higher in the chain. Members with missing or non-numeric positions are treated as position 0. - **`parentChild`**: `{ "parentCollaboratorId": }`. The collaborator id of this member's direct upline. Null (or a pointer that doesn't match a member of the group) means this member is a root of the tree. Switching a group's structure does not migrate per-member metadata. Resolvers tolerate unknown keys. Switching from `linearChain` back to `flat` leaves position values in place, and they're simply ignored. ## REST endpoints | Method and path | What it does | |---|---| | `GET /collaborator-groups` | Lists groups. The `fields` query parameter is required. | | `GET /collaborator-groups/{id}` | Reads one group. | | `POST /collaborator-groups` | Creates a group, optionally with an initial `members` array. | | `PUT /collaborator-groups/{id}` | Updates `name`, `description`, or `structure`. Member changes do not go through this endpoint. | | `DELETE /collaborator-groups/{id}` | Deletes the group and cascades its member rows. Does not detach program or distributor bindings, which then point at a missing group. | | `GET /collaborator-groups/{id}/members` | Lists the members of one group. | | `POST /collaborator-groups/{id}/members` | Bulk-adds members. Collaborators already in the group are skipped. | | `PUT /collaborator-groups/{id}/members` | Full-replaces the roster, reconciling the submitted list against the current one. | | `DELETE /collaborator-groups/{id}/members/{collaboratorId}` | Removes one member. Returns 204, or 404 when the membership row does not exist. | | `GET /collaborator-groups/structures` | Lists the installed structure resolvers and the walker capabilities each advertises. | All of these require the Update capability on `CollaboratorGroup`, held by the `administrator` and `siren_platform_manager` roles. See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## Binding a program or distributor A program or distributor does not store its collaborator-group association on its own row. The binding lives in the shared `wp_siren_configs` table, keyed by: - **Programs**: `(type='program', subtype=, configKey='collaboratorGroupId')` - **Distributors**: `(type='distributor', subtype=, configKey='collaboratorGroupId')` You set the binding by including `collaboratorGroupId` in the body of a `POST` or `PUT` against the program or distributor itself. There is no separate binding endpoint: ``` PUT /programs/{id} ``` ```json { "collaboratorGroupId": 12 } ``` A listener on the program/distributor action event writes the config row and broadcasts a `ProgramBoundToCollaboratorGroup` (or `DistributorBoundToCollaboratorGroup`) event. Passing `null`, `""`, or `0` clears the binding. Each program or distributor can be bound to at most one collaborator group. Once bound, the [upline cascade](/documentation/calculation-strategies/upline-cascade) and [downline cascade](/documentation/calculation-strategies/downline-cascade) calcs walk the group on every qualifying engagement. ## See also - [What are collaborator groups](/documentation/general/what-are-collaborator-groups): operator-facing concept page. - [Choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure): picker guide for `flat`, `linearChain`, and `parentChild`. - [Flat structure](/documentation/collaborator-group-structures/flat), [linear chain](/documentation/collaborator-group-structures/linear-chain), [parent-child tree](/documentation/collaborator-group-structures/parent-child): per-structure detail. - [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group): tutorial covering the same surface from the admin UI. ## Collaborator Product Sold Source: https://www.sirenaffiliates.com/documentation/general/collaborator-product-sold When a customer buys a product owned by a collaborator, the Collaborator Product Sold event is triggered. import EventFlow from "@/components/content/EventFlow.astro"; When a customer purchases a product that's owned by a [collaborator](/documentation/general/what-is-a-collaborator), the "Collaborator Product Sold" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the sale to the collaborator who owns the product. ## How it's typically used This trigger powers creator and marketplace-style setups, where your site sells products that other people made. Instead of paying a flat commission for driving traffic, you're paying the creator a royalty every time their product sells. You handle the storefront, the checkout, and the fulfillment, and the creator gets credited for each sale of anything they own. Products are assigned to collaborators through a field Siren adds to the product edit screen in your commerce plugin. Once a product has an owner, every sale of that product creates an engagement for that collaborator automatically. The [product royalty program](/recipes/product-royalty-program) recipe is the ready-to-use version of this pattern. ## CollaboratorAccountReady Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-system/collaborator-account-ready Fires when a collaborator's account is fully set up and ready to participate in programs. # CollaboratorAccountReady `CollaboratorAccountReady` fires when a collaborator's account is fully set up and ready to participate in programs. At this point, the collaborator record has been persisted, any auto-assigned programs have been applied, and the account is in a usable state. This is the event to listen to when you need to trigger welcome emails, provision external accounts, or notify administrators of a new signup. The event ID is `collaborator_account_ready`, and its fully qualified class is `Siren\Collaborators\Core\Events\CollaboratorAccountReady`. ## What does this event carry? The event carries the completed `Collaborator` model, which provides access to the collaborator's profile, assigned programs, and account status. ```php use Siren\Collaborators\Core\Events\CollaboratorAccountReady; public function handle(Event $event): void { $collaborator = $event->getCollaborator(); // The collaborator is fully set up at this point. // Use this to send welcome emails, sync to external systems, // or trigger any post-registration workflows. } ``` ## How does this relate to other registration events? The registration process fires several events in sequence. `CollaboratorSubmissionReceived` fires first and allows listeners to modify the submission before the record is created. After the record is persisted, `CollaboratorSubmissionComplete` fires to signal that the record exists but setup may still be in progress. `CollaboratorAccountReady` fires last, once everything is finalized. If you need to modify the registration data, listen to `CollaboratorSubmissionReceived`. If you need to react to the fully provisioned account, `CollaboratorAccountReady` is the right choice. The distinction matters because listeners on the earlier events should not assume that programs, permissions, or other post-creation setup steps have finished. For the full lifecycle of system events and how they connect to the rest of Siren's architecture, see the [System Events overview](/documentation/developer-reference/events-system). ## CollaboratorAddedToCollaboratorGroup Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group Fires when a collaborator becomes a member of a collaborator group. # CollaboratorAddedToCollaboratorGroup `CollaboratorAddedToCollaboratorGroup` fires every time a collaborator joins a [collaborator group](/documentation/general/what-are-collaborator-groups), whether through the dedicated add-members endpoint or as part of a bulk member replacement. One event fires per new member, even when many are added in a single request. It fires only for genuinely new members, so re-sending a collaborator who is already in the group does not fire it again. The event is broadcast after the membership is saved, so the new member is already queryable in the group when your handler runs. The event ID is `collaborator_added_to_collaborator_group`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorAddedToCollaboratorGroup`. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) for how to register one and the [events introduction](/documentation/developer-reference/events-introduction) for the dispatch model. ## What does this event carry? The event carries the group id, the collaborator id, and the metadata array stored on the membership. For [linear chain](/documentation/collaborator-group-structures/linear-chain) groups the metadata usually contains a `position`. For [parent-child](/documentation/collaborator-group-structures/parent-child) groups it usually contains a `parentCollaboratorId`. The metadata array is whatever the [structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) needs. The metadata is what was supplied when the member was added, so it is an empty array if none was sent. Siren does not fill in defaults here. A linear-chain member added without a `position`, for example, carries an empty metadata array and is treated as position 0 only later, when the structure reads the group. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorAddedToCollaboratorGroup; class HandleNewMember implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorAddedToCollaboratorGroup) { return; } $groupId = $event->getGroupId(); $collaboratorId = $event->getCollaboratorId(); $metadata = $event->getMetadata(); // metadata is structure-specific: ['position' => 2], etc. } } ``` ## How does it fit? This is the event that changes who the [cascade](/documentation/general/what-is-a-cascade) will visit. When the next trigger fires against a program or distributor bound to this group, the new member is in scope. The activity feed listens here, and so does any onboarding flow that wants to send a welcome notification when a collaborator joins a group. ## Related events - [CollaboratorRemovedFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-removed-from-collaborator-group) fires when a member leaves the group. - [CollaboratorGroupMemberMetadataChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed) fires when a member's `position`, `parentCollaboratorId`, or other metadata changes. - The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family, which you need together to keep an external mirror of group membership in sync. ## CollaboratorGroupCreated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-created Fires when a new collaborator group is created via the REST API. # CollaboratorGroupCreated `CollaboratorGroupCreated` fires immediately after a new [collaborator group](/documentation/general/what-are-collaborator-groups) is persisted through the REST API. The event marks the birth of the group as a domain entity. At this point the group has a name, a [structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure), and an id, but no members and no bound programs or distributors yet. The event ID is `collaborator_group_created`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupCreated`. It is fired by the create-group REST endpoint, so a group created by another path does not fire it. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) for how to register one and the [events introduction](/documentation/developer-reference/events-introduction) for the dispatch model. ## What does this event carry? The event carries one value: the id of the freshly created group. The group is already saved when the event fires, so listeners that need the full record use the id to load it from the [CollaboratorGroup datastore](/documentation/resource-reference/collaborator-groups). The name and structure live on that record, not on the event. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupCreated; class LogNewCollaboratorGroup implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupCreated) { return; } $groupId = $event->getGroupId(); // Load the full group from the datastore if you need name / structure } } ``` ## How does it fit? The activity feed listens for this event to record a "group created" entry. Anything that needs to seed default state for a new group (a default per-layer payout config, a downstream lookup cache, an external sync) hooks here too. The follow-up lifecycle events ([CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group), [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group)) carry the rest of the configuration as the operator wires the group up. ## Related events The group's own lifecycle continues with [CollaboratorGroupRenamed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-renamed), [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed), and [CollaboratorGroupDeleted](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted). Membership and bindings are covered by [CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) and [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group). The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## CollaboratorGroupDeleted Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted Fires when a collaborator group is deleted via the REST API. Broadcast before the underlying row is removed. # CollaboratorGroupDeleted `CollaboratorGroupDeleted` fires when an operator deletes a [collaborator group](/documentation/general/what-are-collaborator-groups) through the REST API. The event is broadcast *before* the actual datastore delete runs. This lets listeners hydrate any context they need (the group's name, its current members) while the row is still readable. The event ID is `collaborator_group_deleted`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupDeleted`. It is fired by the delete-group REST endpoint, so a group removed by another path does not fire it. The controller broadcasts it and then calls the datastore delete, so the event marks a delete that is about to run, not one that is already committed. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) for how to register one and the [events introduction](/documentation/developer-reference/events-introduction) for the dispatch model. ## What does this event carry? The event carries one value: the id of the group being deleted. Because the broadcast is pre-delete, listeners can still call into the datastore to load the full record during the handler. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupDeleted; class SnapshotGroupBeforeDelete implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupDeleted) { return; } $groupId = $event->getGroupId(); // The group row is still in the datastore at this point, // safe to read its name, structure, members for an audit log } } ``` ## Related events [CollaboratorGroupCreated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-created) and [CollaboratorGroupRenamed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-renamed) cover the start of the group lifecycle, and [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed) covers structure edits. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## How does it fit? The note system uses the pre-delete window to capture the group name on the activity entry it writes. Once the row is gone, the standard source-data resolver would return null, so the handler is your one chance to read anything off the group. Capture whatever you need (the name, the members, the bindings) inside the handler, because it is unrecoverable once the handler returns. The delete is not announced piece by piece. When the group is removed, its membership rows are deleted in bulk, and no per-member [CollaboratorRemovedFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-removed-from-collaborator-group) events fire for them. Programs and distributors bound to the group are not detached either. Their binding stays on record pointing at the now-deleted group, and a cascade or eligibility check bound to it [fails closed](/documentation/calculation-strategies/cascade-troubleshooting) and credits no one until you re-bind or remove it. So if you mirror Siren state externally, do not wait for member or binding events on a group delete. Snapshot the members and bindings in this handler and tear them down on your side. ## CollaboratorGroupMemberMetadataChanged Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed Fires when a collaborator group member's metadata is updated in place. # CollaboratorGroupMemberMetadataChanged `CollaboratorGroupMemberMetadataChanged` fires when an existing membership has its metadata rewritten. For example, an operator changes a collaborator's `position` in a [linear chain](/documentation/collaborator-group-structures/linear-chain), or moves a collaborator under a different parent in a [parent-child](/documentation/collaborator-group-structures/parent-child) tree. The event only fires when the metadata actually changes. A write that produces the same array is a no-op. It fires once per member whose metadata changed, so a single update that reorders a chain fires one event for each member whose `position` moved, not one event for the whole reorder. The same is true of a bulk member replacement (a PUT of the full member list): every member whose metadata differs from before fires its own event. Adding a brand-new member fires [CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) instead, not this event. The event ID is `collaborator_group_member_metadata_changed`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupMemberMetadataChanged`. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) for how to register one and the [events introduction](/documentation/developer-reference/events-introduction) for the dispatch model. ## What does this event carry? The event carries the group id, the collaborator id, the metadata array before the change, and the metadata array after the change. Both arrays are keyed by the fields the group's structure uses. A [linear chain](/documentation/collaborator-group-structures/linear-chain) member carries a `position` (an integer), and a [parent-child](/documentation/collaborator-group-structures/parent-child) member carries a `parentCollaboratorId` (an integer, or null for a root). A custom [structure resolver](/documentation/extensions/collaborator-group-structures) can define its own fields. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupMemberMetadataChanged; class HandleMemberMetadataChange implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupMemberMetadataChanged) { return; } $groupId = $event->getGroupId(); $collaboratorId = $event->getCollaboratorId(); $previous = $event->getPreviousMetadata(); $next = $event->getNewMetadata(); // e.g. for linearChain: ['position' => 2] became ['position' => 1] } } ``` ## How does it fit? Metadata is the structural payload. It's what tells the [cascade](/documentation/general/what-is-a-cascade) where a member sits relative to their peers. A position swap in a linear chain re-orders the upline. A parent change in a parent-child tree re-roots a whole subtree under a new ancestor. The activity feed records the before/after so an operator can see exactly what moved. Because one reorder can move many members, a cache or external mirror that keys off member position should refresh the whole group rather than just the one collaborator named in the event. Changing the group's own structure is a separate [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed) event. A structure change does not rewrite member metadata, so it does not fire this event. The membership rows keep their existing keys, which the new structure may simply ignore. ## Related events [CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) and [CollaboratorRemovedFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-removed-from-collaborator-group) cover membership, and [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed) covers a structure switch. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## CollaboratorGroupMemberResolverRegistryInitiated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-resolver-registry-initiated Broadcast once when the CollaboratorGroupMember field-resolver registry is first read. Listeners register per-field getter closures. # CollaboratorGroupMemberResolverRegistryInitiated `CollaboratorGroupMemberResolverRegistryInitiated` is broadcast exactly once, on the first read of the CollaboratorGroupMember field-resolver registry. It's the registration seam for any module that wants to expose a new field on the membership rows returned by the [CollaboratorGroup resource](/documentation/resource-reference/collaborator-groups). Adding a field here surfaces it in the members list returned alongside the group. The event ID is `collaborator_group_member_resolver_registry_initiated`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupMemberResolverRegistryInitiated`. To run code when it fires, register a handler with Siren's event system. Because the registry is built exactly once on first read, register your handler during plugin initialization, the same way the core resolvers are wired, so it is in place before that first read. A handler added after the first read misses the window and its field never appears. See [listeners](/documentation/extensions/listeners), the [events introduction](/documentation/developer-reference/events-introduction), and the [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups). ## What does this event carry? The event extends the framework's `FieldResolverRegistryInitiatedEvent` abstract and exposes one writer: `addResolver(string $field, callable $resolver)`. The resolver closure receives the `CollaboratorGroupMember` model and returns whatever the field should expose on the API row. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupMemberResolverRegistryInitiated; use Siren\Plus\Core\Groups\Models\CollaboratorGroupMember; class RegisterLifetimePayoutResolver implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupMemberResolverRegistryInitiated) { return; } $event->addResolver('lifetimePayout', function (CollaboratorGroupMember $member) { // Derive the value from whatever store you keep it in return 0.0; }); } } ``` ## How does it fit? This is the same registry pattern as the [group-level field registry](/documentation/developer-reference/events-collaborator-groups/collaborator-group-resolver-registry-initiated) and the [structure-resolver registry](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-resolver-registry-initiated), scoped to the per-member rows. Core fields and any custom fields are registered the same way, by listening to this event and calling `addResolver`. The core resolvers (`id`, `groupId`, `collaboratorId`, `metadata`, `dateCreated`, `dateModified` plus `collaboratorName`, `collaboratorEmail`, and `collaboratorStatus`) are all registered through `RegisterCoreCollaboratorGroupMemberResolvers` listening to this event. The first six fields read straight off the membership row. The last three are joined from the collaborator record. A resolver that joins another datastore runs once per member on every list request, so keep it cheap and defend against missing rows (return `null` rather than throwing) so the list endpoint keeps working against orphaned memberships. On a large group a careless join becomes an N+1 query. Field names are unique in the registry, so registering a resolver for a name that already exists (a core field like `metadata`) replaces it. Use a distinct name unless you mean to override, and return JSON-friendly data, since the value is serialized onto the API row. See [custom structure resolvers](/documentation/extensions/collaborator-group-structures) for the same registry shape applied to structures. ## CollaboratorGroupRenamed Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-renamed Fires when an existing collaborator group's name is changed via the REST API. # CollaboratorGroupRenamed `CollaboratorGroupRenamed` fires when an operator changes the display name of an existing [collaborator group](/documentation/general/what-are-collaborator-groups) through the REST API. Only renames trigger this event. Updates that leave the name unchanged are silently skipped so the activity feed stays free of noise. The event ID is `collaborator_group_renamed`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupRenamed`. It is fired by the update-group REST endpoint, so a name changed by another path does not fire it. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the group id plus both the previous and the new name. That before/after pair is what lets a listener render a meaningful audit entry without having to re-query the datastore for prior state. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupRenamed; class HandleGroupRename implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupRenamed) { return; } $groupId = $event->getGroupId(); $previous = $event->getPreviousName(); $next = $event->getNewName(); // e.g. "Renamed 'Affiliate Tier 1' to 'Gold Affiliates'" } } ``` ## How does it fit? Renames are a low-stakes lifecycle event. They don't disturb members, bindings, or [cascade](/documentation/general/what-is-a-cascade) behavior. The most common listener is the activity feed. External integrations that mirror Siren state into another system (a CRM, a data warehouse) listen here to keep their copy of the name in sync. A single update request can change the name, the description, and the structure at once, and each change fires its own event. So a rename made in the same request as a structure change fires both this event and [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed). Handle them independently. ## Related events [CollaboratorGroupCreated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-created), [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed), and [CollaboratorGroupDeleted](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted) cover the rest of the group's lifecycle. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## CollaboratorGroupResolverRegistryInitiated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-resolver-registry-initiated Broadcast once when the CollaboratorGroup field-resolver registry is first read. Listeners register per-field getter closures. # CollaboratorGroupResolverRegistryInitiated `CollaboratorGroupResolverRegistryInitiated` is broadcast exactly once, on the first read of the CollaboratorGroup field-resolver registry. It's the registration seam for any module that wants to expose a new field on the [CollaboratorGroup resource](/documentation/resource-reference/collaborator-groups). The field becomes part of the API response and the admin list view without touching the core Plus code. The event ID is `collaborator_group_resolver_registry_initiated`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupResolverRegistryInitiated`. To run code when it fires, register a handler with Siren's event system. Because the registry is built exactly once on first read, register your handler during plugin initialization, the same way the core resolvers are wired, so it is in place before that first read. A handler added after the first read misses the window and its field never appears. See [listeners](/documentation/extensions/listeners), the [events introduction](/documentation/developer-reference/events-introduction), and the [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups). ## What does this event carry? The event extends the framework's `FieldResolverRegistryInitiatedEvent` abstract and exposes one writer: `addResolver(string $field, callable $resolver)`. The resolver is a closure that receives the `CollaboratorGroup` model and returns whatever value that field should expose. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupResolverRegistryInitiated; use Siren\Plus\Core\Groups\Models\CollaboratorGroup; class RegisterMemberCountResolver implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupResolverRegistryInitiated) { return; } $event->addResolver('memberCount', function (CollaboratorGroup $group) { // Return whatever derived value this field should expose return 0; }); } } ``` ## How does it fit? This is the standard three-tier registry pattern Siren uses for extension points: a registry (the field-resolver store), an event (this one, broadcast on first read), and a provider service that lazy-builds the registry when something asks for a field. The core resolvers (`id`, `name`, `description`, `structure`) are registered the same way, through `RegisterCoreCollaboratorGroupResolvers` listening to this event. Higher tiers and third-party integrations follow the same pattern to add their own fields. The per-member rows have their own [field-resolver registry](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-resolver-registry-initiated) with the same shape, and [custom structure resolvers](/documentation/extensions/collaborator-group-structures) wire structures the same way. A resolver runs once per group row on every list request, so keep it cheap. The `memberCount` example above would query the member datastore per group, which turns a list of groups into an N+1 query, so cache or batch that kind of lookup. Field names are unique in the registry, so registering a resolver for a name that already exists (a core field like `name`) replaces it. Use a distinct name unless you mean to override, and return JSON-friendly data, since the value is serialized onto the API row and the admin list. ## CollaboratorGroupStructureChanged Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed Fires when a collaborator group's structure resolver id changes via the REST API. # CollaboratorGroupStructureChanged `CollaboratorGroupStructureChanged` fires when an operator swaps the [structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) of an existing [collaborator group](/documentation/general/what-are-collaborator-groups), for example moving a group from `flat` to `linearChain` so it can support cascades. Per-member metadata is not migrated automatically when the structure changes, so listeners that depend on the new shape should expect to read fresh metadata for each member. The event ID is `collaborator_group_structure_changed`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorGroupStructureChanged`. It is fired by the update-group REST endpoint when the structure field changes. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the group id, the previous structure resolver id (the string identifier like `flat` or `linearChain`), and the new structure resolver id. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorGroupStructureChanged; class InvalidateCalcOptionsOnStructureChange implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupStructureChanged) { return; } $groupId = $event->getGroupId(); $from = $event->getPreviousStructure(); $to = $event->getNewStructure(); // The calc-picker capability set may have changed for any // program or distributor bound to this group } } ``` ## How does it fit? A structure swap can quietly invalidate downstream choices. Calc strategies that require the `hasLayer` capability (upline cascade, downline cascade) only make sense against `linearChain` and `parentChild` groups. Flipping a group back to `flat` hides those options in the picker on the next page load, but it does not touch a cascade calc that is already saved against the group. That saved cascade stays bound and [fails closed](/documentation/calculation-strategies/cascade-troubleshooting) at payout time, crediting no one, until an operator picks a compatible calc or restores a layered structure. So a layer-losing swap is a payout-breaking change, not a cosmetic one, and a handler should treat it as an alert rather than a quiet log. Going the other way has its own catch. A swap from `flat` to a layered structure does not fill in member positions or parents, so until an operator sets them, every member sits at the default (position 0, or no parent), and a cascade runs in that degenerate order rather than the hierarchy the operator intends. See [linear chain](/documentation/collaborator-group-structures/linear-chain) and [parent-child](/documentation/collaborator-group-structures/parent-child) for how to set them. This event carries no per-member detail, and the member rows are not rewritten by the swap, so no per-member [metadata-changed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed) events fire alongside it. If you mirror Siren state, treat this event as a signal to re-read the whole group's members and re-interpret their metadata under the new structure. ## Related events A structure swap and a [rename](/documentation/developer-reference/events-collaborator-groups/collaborator-group-renamed) made in one request fire both events. [CollaboratorGroupCreated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-created) and [CollaboratorGroupDeleted](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted) cover the rest of the lifecycle. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## CollaboratorGroupStructureResolverRegistryInitiated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-resolver-registry-initiated Broadcast once when the collaborator group structure-resolver registry is first read. Listeners register their structure resolvers via addStrategy(). # CollaboratorGroupStructureResolverRegistryInitiated `CollaboratorGroupStructureResolverRegistryInitiated` is broadcast exactly once, on the first read of the structure-resolver provider service. It's the registration seam for any module that wants to add a new [collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) (a layout that decides how members relate to each other, such as flat, linear chain, or parent-child). A structure registered here becomes selectable on a group without touching the Plus tier that owns the registry. The event ID is `collaborator_group_structure_resolver_registry_initiated`, and its fully qualified class is `Siren\Plus\Core\Groups\Structure\Events\CollaboratorGroupStructureResolverRegistryInitiated`. To run code when it fires, register a handler with Siren's event system. Because the registry is built exactly once on first read, register your handler during initialization so it is in place before that first read. A handler added after the first read misses the window and its structure never registers. See [listeners](/documentation/extensions/listeners), the [events introduction](/documentation/developer-reference/events-introduction), and the [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups). ## What does this event carry? The event carries the `CollaboratorGroupStructureResolverRegistry` it populates and a `PHPNomad\Di\Interfaces\InstanceProvider`, both held as protected readonly constructor properties. It exposes two writers: - `addStrategy(string $resolverClass): void` takes a `class-string`. It stores a lazy factory, so the resolver class is only instantiated when something actually requests that structure. The registry key is `$resolverClass::getId()`, the structure id string such as `flat`, `linearChain`, or `parentChild`. - `deleteStrategy(string $id): void` removes a registered resolver by its id. Unlike the group and member [field-resolver registries](/documentation/developer-reference/events-collaborator-groups/collaborator-group-resolver-registry-initiated), which register a closure per field with `addResolver`, this registry registers a whole resolver class with `addStrategy`, keyed by the class's own `getId()`. The id is read without instantiating the class, so `getId()` must be static. The resolver itself is built through the carried `InstanceProvider` the first time its structure is requested, so its constructor dependencies must be resolvable through Siren's container. Registering a strategy whose `getId()` matches one already in the registry replaces it, so use a unique id unless you mean to override a built-in. Use `deleteStrategy` to remove or swap a strategy during this registration window. Removing a structure that existing groups are already configured to use leaves those groups unable to resolve their structure, so cascades bound to them fail closed. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Structure\Events\CollaboratorGroupStructureResolverRegistryInitiated; class RegisterMyStructureResolver implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupStructureResolverRegistryInitiated) { return; } // Pass the resolver class string; the registry keys it by // MyStructureResolver::getId() and instantiates it lazily. $event->addStrategy(MyStructureResolver::class); } } ``` ## How does it fit? This is the standard three-tier registry pattern Siren uses for extension points: a registry (the structure-resolver store), an event (this one, broadcast on first read), and a provider service that lazy-builds the registry the first time a structure is requested. Plus registers only the [flat structure](/documentation/collaborator-group-structures/flat) resolver through `RegisterPlusCollaboratorGroupStructureResolvers` listening to this event. The event is the seam the Pro tier uses to add the [linear chain](/documentation/collaborator-group-structures/linear-chain) and [parent-child](/documentation/collaborator-group-structures/parent-child) structure resolvers. Pro's `RegisterProCollaboratorGroupStructureResolvers` listens to the same event and calls `addStrategy()` for `LinearChainCollaboratorGroupStructureResolver` and `ParentChildCollaboratorGroupStructureResolver`. Because registration happens through the event rather than through edits to Plus, Pro adds its hierarchical structures with no Plus modifications required. A structure resolver can also advertise walker capabilities, such as `HAS_LAYER`, through the `HasProvidedWalkerCapabilities` interface. Cascades require `HAS_LAYER`, so the [linear chain](/documentation/collaborator-group-structures/linear-chain) and [parent-child](/documentation/collaborator-group-structures/parent-child) resolvers advertise it while [flat](/documentation/collaborator-group-structures/flat) does not. A custom structure that omits it is selectable on a group but cannot drive an upline or downline cascade, which is a quiet way to ship a structure that looks fine but never pays out a cascade. See [custom structure resolvers](/documentation/extensions/collaborator-group-structures) for a full walkthrough of building and registering your own resolver, including which capabilities to declare. ## CollaboratorRemovedFromCollaboratorGroup Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/collaborator-removed-from-collaborator-group Fires when a collaborator is removed from a collaborator group. # CollaboratorRemovedFromCollaboratorGroup `CollaboratorRemovedFromCollaboratorGroup` fires when a collaborator stops being a member of a [collaborator group](/documentation/general/what-are-collaborator-groups). One event fires per removed member, whether the removal came through the single-member endpoint or as part of a bulk member replacement. The event ID is `collaborator_removed_from_collaborator_group`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\CollaboratorRemovedFromCollaboratorGroup`. It fires after the membership row is deleted, through either the single-member remove endpoint or a bulk member replacement. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the group id and the collaborator id. The previous metadata is not included. By the time a listener runs, the membership row is gone and only the identity pair remains. If your cleanup needs the removed member's former `position` or `parentCollaboratorId`, capture it on an earlier [added](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) or [metadata-changed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed) event and store it yourself, because it is unrecoverable here. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\CollaboratorRemovedFromCollaboratorGroup; class HandleRemovedMember implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorRemovedFromCollaboratorGroup) { return; } $groupId = $event->getGroupId(); $collaboratorId = $event->getCollaboratorId(); // The collaborator is no longer reachable through this group's cascade } } ``` ## How does it fit? A removal changes the shape of the group for the next [cascade](/documentation/general/what-is-a-cascade). In a [linear chain](/documentation/collaborator-group-structures/linear-chain) the remaining members keep their stored positions, so the numbers are no longer contiguous, but that gap is harmless. Layers are counted by rank order, not by the position value, so the remaining members close ranks and the chain cascades normally. Re-sequencing positions is cosmetic, not required. In a [parent-child](/documentation/collaborator-group-structures/parent-child) tree, removing a parent leaves its children with a parent reference that now points outside the group, so each of those children becomes a root. Their own subtrees stay intact but are detached from everything above the removed parent, which quietly cuts that branch out of any cascade running through it. Only the removed member fires an event. The children's rows are not rewritten (their re-rooting happens when the structure is next resolved), so no event fires for them. A listener or mirror that tracks the tree must re-evaluate the removed member's children itself. The activity feed records each removal. Cleanup listeners use the event to tear down any per-member state they were keeping. A single bulk member replacement (a PUT of the full member list) fans out into one removed event per dropped member, plus [added](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) events for new members and [metadata-changed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed) events for members whose metadata changed. ## Related events [CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group) and [CollaboratorGroupMemberMetadataChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed) are the other membership events, and [CollaboratorGroupDeleted](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted) removes every member at once without firing per-member removed events. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## Collaborators Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators Collaborator records — data model, status lifecycle, REST API, PHP data access, aliases, and program membership management. import CodeTabs from "@/components/content/CodeTabs.astro"; # Collaborators A collaborator is someone who promotes your products or services in exchange for rewards. This includes affiliates, ambassadors, referral partners, and influencers. Collaborators are the central identity in Siren's attribution pipeline. Each collaborator has a unique referral code (an [alias](/documentation/resource-reference/aliases)) used to track [engagements](/documentation/resource-reference/engagements), which flow downstream into [conversions](/documentation/resource-reference/conversions) and [obligations](/documentation/resource-reference/obligations). Collaborators are enrolled in one or more [programs](/documentation/resource-reference/programs) and assigned to [distributors](/documentation/resource-reference/distributors), which together determine how they earn and what they earn. ## The collaborator object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Full name | `fullName` | `getFullName()` | string | The collaborator's full name | | Nickname | `nickname` | `getNickname()` | string | Display name used in the collaborator portal and public-facing contexts | | Email | `email` | `getEmail()` | string | Email address (unique per organization) | | Status | `status` | `getStatus()` | string | Current status: `active`, `inactive`, `pending`, `rejected`, or `deleted` | | Created | `createdDate` | `getCreatedDate()` | datetime | When the record was created | | Modified | `modifiedDate` | `getModifiedDate()` | datetime | When the record was last updated | ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested: | Field | Type | Description | |---|---|---| | `programs` | array | Array of programs the collaborator is enrolled in, each with `id` and `name` | | `distributors` | array | Array of distributors the collaborator is assigned to, each with `id` and `name` | | `distributorId` | integer or null | ID of the collaborator's first distributor (for backward compatibility when a single distributor is expected) | | `referralCode` | string or null | The collaborator's current tracking alias code | | `aliases` | array | All alias codes for the collaborator, each with `code`, `type`, and `issuedDate` | | `unfulfilledObligationCount` | integer | Count of pending obligations owed to this collaborator | | `fulfilledObligationCount` | integer | Count of completed obligations for this collaborator | ## Status lifecycle | Status | Description | |---|---| | `pending` | Awaiting review. This is the initial state for collaborators who sign up through the public form when approval is required. | | `active` | The collaborator can generate referrals and earn rewards. | | `inactive` | The collaborator's account is paused. No new engagements or conversions will be attributed. | | `rejected` | The application was denied. Can only be set via update, not at creation time. | | `deleted` | Soft-deleted. A second DELETE call permanently removes the record. | ## Accessing collaborator data ```bash # List active collaborators curl -X GET "https://your-site.com/wp-json/siren/v1/collaborators?status=active" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single collaborator with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/collaborators/42?fields=id,fullName,email,programs,referralCode,aliases" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Collaborators\Core\Datastores\Collaborator\Interfaces\CollaboratorDatastore; class MyService { protected CollaboratorDatastore $collaborators; public function __construct(CollaboratorDatastore $collaborators) { $this->collaborators = $collaborators; } public function getActiveCollaborators(): array { return $this->collaborators->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); } } ``` ```php use Siren\Collaborators\Core\Facades\Collaborators; $active = Collaborators::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); $collaborator = Collaborators::find(42); ``` > Most collaborator records are created as side effects of Siren's [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). When a new affiliate registers or is imported, the system fires domain events that handle record creation, program enrollment, and alias assignment. You should reach for the datastore directly when you need to read collaborator data for display, look up a collaborator to make decisions in custom logic, or manage program membership in a migration script. ## PHP domain methods ### Looking up by email `getByEmail(string $email)` retrieves a single collaborator by email address. Throws `RecordNotFoundException` if no collaborator exists with that email. ```php use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $collaborator = $this->collaborators->getByEmail('jane@example.com'); $name = $collaborator->getFullName(); } catch (RecordNotFoundException $e) { // No collaborator with that email } ``` ```php use Siren\Collaborators\Core\Facades\Collaborators; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $collaborator = Collaborators::getByEmail('jane@example.com'); $name = $collaborator->getFullName(); } catch (RecordNotFoundException $e) { // No collaborator with that email } ``` ### Looking up by WordPress user ID `getCollaboratorFromUserId(int $userId)` finds the collaborator linked to a WordPress user through Siren's mapping system. This is the bridge between WordPress's user table and Siren's collaborator records. Throws `RecordNotFoundException` if no mapping exists for that user. ```php // Get the collaborator record for the current WordPress user $collaborator = $this->collaborators->getCollaboratorFromUserId( get_current_user_id() ); echo $collaborator->getNickname(); ``` ```php use Siren\Collaborators\Core\Facades\Collaborators; // Get the collaborator record for the current WordPress user $collaborator = Collaborators::getCollaboratorFromUserId( get_current_user_id() ); echo $collaborator->getNickname(); ``` Under the hood, this method queries the [Mappings](/documentation/resource-reference/mappings) datastore for a record linking the external user ID to an internal collaborator ID, then fetches the collaborator by that ID. ### Listing collaborators in a program `getCollaboratorsInProgram(int $programId, int $limit = 10, int $offset = 0)` returns an array of `Collaborator` models enrolled in the given program. Supports pagination through the limit and offset parameters. ```php // Get the first 25 collaborators in program 5 $collaborators = $this->collaborators->getCollaboratorsInProgram(5, 25, 0); foreach ($collaborators as $collaborator) { echo $collaborator->getFullName() . ' (' . $collaborator->getEmail() . ')' . PHP_EOL; } ``` ```php use Siren\Collaborators\Core\Facades\Collaborators; // Get the first 25 collaborators in program 5 $collaborators = Collaborators::getCollaboratorsInProgram(5, 25, 0); foreach ($collaborators as $collaborator) { echo $collaborator->getFullName() . ' (' . $collaborator->getEmail() . ')' . PHP_EOL; } ``` ### Managing program membership `addCollaboratorToProgram(int $collaboratorId, int $programId)` enrolls a collaborator in a program. Throws `DuplicateEntryException` if the collaborator is already enrolled. `removeCollaboratorFromProgram(int $collaboratorId, int $programId)` removes a collaborator from a program. ```php use PHPNomad\Datastore\Exceptions\DuplicateEntryException; try { $this->collaborators->addCollaboratorToProgram(12, 5); } catch (DuplicateEntryException $e) { // Already enrolled } // Remove from a different program $this->collaborators->removeCollaboratorFromProgram(12, 3); ``` ```php use Siren\Collaborators\Core\Facades\Collaborators; use PHPNomad\Datastore\Exceptions\DuplicateEntryException; try { Collaborators::addCollaboratorToProgram(12, 5); } catch (DuplicateEntryException $e) { // Already enrolled } // Remove from a different program Collaborators::removeCollaboratorFromProgram(12, 3); ``` These methods manage the junction table that links collaborators to programs. In most cases, program enrollment happens automatically through Siren's event pipeline, but these methods are useful for migration scripts or custom enrollment logic. ### Group membership A collaborator can also belong to one or more [collaborator groups](/documentation/resource-reference/collaborator-groups), the named clusters that programs and distributors bind to for cascade calculations. Group membership is managed on the group, not on the collaborator. Use the member endpoints on `/collaborator-groups/{id}/members` to add or remove a collaborator from a group. When a collaborator is deleted, Siren removes that collaborator from every group they belonged to. Only the membership rows are removed. The groups themselves and the other members' metadata are left in place. ## Aliases Aliases are the codes that identify a collaborator in tracking links and coupon systems. A collaborator can have multiple aliases over time — for example, if they change their tracking slug — and the system keeps a historical record so that old links continue to resolve correctly. For a focused reference on the alias datastore, including all query methods and historical lookups, see [Aliases](/documentation/resource-reference/aliases). Each alias has a type (such as "tracking" or "coupon"), a code string, and an issued date. The alias datastore returns the most-recently issued alias by default, but accepts an optional `DateTime` parameter to look up who owned a particular code at a specific point in history. ### Alias model The `Alias` model (`Siren\Collaborators\Core\Models\Alias`) has the following fields: | Field | PHP getter | Type | Description | |---|---|---|---| | Code | `getCode()` | string | The alias code (e.g. a tracking slug or coupon code) | | Type | `getType()` | string | The alias type, such as "tracking" or "coupon" | | Collaborator | `getCollaboratorId()` | integer | The collaborator this alias belongs to | | Issued | `getIssuedDate()` | datetime | When this alias was assigned | ### Accessing the alias datastore ```php use Siren\Collaborators\Core\Datastores\CollaboratorAliases\Interfaces\CollaboratorAliasesDatastore; class MyService { protected CollaboratorAliasesDatastore $aliases; public function __construct(CollaboratorAliasesDatastore $aliases) { $this->aliases = $aliases; } } ``` ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; $alias = CollaboratorAliases::getAliasByCode('janedoe', 'tracking'); ``` ### Looking up an alias by code `getAliasByCode(string $code, string $type, ?DateTime $before = null)` finds the alias record matching a specific code and type. Since the same code can be reassigned over time, this returns the most-recently issued match. Pass a `DateTime` to the `$before` parameter to find who owned the code at that point in history. Throws `RecordNotFoundException` if no matching alias exists. ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; // Who currently owns this tracking code? $alias = CollaboratorAliases::getAliasByCode('janedoe', 'tracking'); $collaboratorId = $alias->getCollaboratorId(); // Who owned it on January 1st? $alias = CollaboratorAliases::getAliasByCode( 'janedoe', 'tracking', new DateTime('2025-01-01') ); ``` ### Getting a collaborator's current alias `getAliasForCollaborator(int $id, string $type, ?DateTime $before = null)` retrieves the alias of a given type for a specific collaborator. Returns the most-recently issued alias of that type. Like `getAliasByCode`, the `$before` parameter supports historical lookups. Throws `RecordNotFoundException` if the collaborator has no alias of that type. ```php use Siren\Collaborators\Core\Facades\CollaboratorAliases; // Get this collaborator's current tracking alias $alias = CollaboratorAliases::getAliasForCollaborator(12, 'tracking'); echo $alias->getCode(); // e.g. "janedoe" // Get their coupon alias $couponAlias = CollaboratorAliases::getAliasForCollaborator(12, 'coupon'); echo $couponAlias->getCode(); // e.g. "JANE20" ``` ### Practical example: displaying a collaborator's dashboard info This example retrieves a collaborator from the currently logged-in WordPress user and displays their tracking link. ```php use Siren\Collaborators\Core\Facades\Collaborators; use Siren\Collaborators\Core\Facades\CollaboratorAliases; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $collaborator = Collaborators::getCollaboratorFromUserId(get_current_user_id()); $alias = CollaboratorAliases::getAliasForCollaborator( $collaborator->getId(), 'tracking' ); echo 'Welcome, ' . esc_html($collaborator->getFullName()) . '!'; echo 'Your referral link: https://example.com/?ref=' . urlencode($alias->getCode()); } catch (RecordNotFoundException $e) { echo 'No collaborator account found for this user.'; } ``` ## Relationships - **[Programs](/documentation/resource-reference/programs)** (enrollment). Collaborators are enrolled in one or more programs, which define how referrals are tracked and how rewards are calculated. Program enrollment is managed through the `programs` field on create and update, or through the `addToProgram` and `removeFromProgram` bulk actions. - **[Distributors](/documentation/resource-reference/distributors)** (assignment). Collaborators are assigned to distributors, which represent the products or services they promote. The `distributors` and `distributorId` extended fields expose these assignments. - **[Engagements](/documentation/resource-reference/engagements)** (downstream). When a collaborator's referral code is used, the system creates an engagement attributed to that collaborator. Engagements feed into the conversion pipeline. - **[Obligations](/documentation/resource-reference/obligations)** (downstream). Approved conversions generate obligations recording what is owed to the collaborator. The `unfulfilledObligationCount` and `fulfilledObligationCount` extended fields provide at-a-glance totals. - **[Collaborator groups](/documentation/resource-reference/collaborator-groups)** (membership). A collaborator can be a member of one or more groups, which programs and distributors bind to for cascade calculations. Deleting a collaborator removes their membership rows from every group they belonged to. - **[Aliases](/documentation/resource-reference/aliases).** Each collaborator has one or more alias codes (including their referral code) used for tracking. The `aliases` extended field returns the full set. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## CollaboratorSubmissionReceived Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-system/collaborator-submission-received Fires when a new collaborator registration request arrives. Allows listeners to modify the submission before the record is created. # CollaboratorSubmissionReceived `CollaboratorSubmissionReceived` fires when a new collaborator registration request arrives. This is a mutable event: listeners can modify the submission before the collaborator record is actually created, making it the right place to auto-assign programs, set default fields, or apply business rules based on where the registration came from. The event ID is `collaborator_submission_received`, and its fully qualified class is `Siren\Collaborators\Core\Events\CollaboratorSubmissionReceived`. ## What does this event carry? The event carries three things: a `CollaboratorBuilder` that holds the in-progress collaborator data, a `CollaboratorSubmissionConfigurationFactory` that provides access to the submission's configuration context, and a `source` string that identifies where the registration request originated. ```php use Siren\Collaborators\Core\Events\CollaboratorSubmissionReceived; public function handle(Event $event): void { $builder = $event->getCollaboratorBuilder(); $configFactory = $event->getCollaboratorSubmissionConfigurationFactory(); $source = $event->getSource(); // Listeners can modify the builder to set defaults, // auto-assign programs, or apply rules based on the source } ``` ## When would you listen to this event? The `source` string opens up conditional logic based on where the collaborator signed up. A listener could auto-assign collaborators from a specific landing page to a particular program, or set default commission tiers based on the registration source. Because the event fires before the record is persisted, any modifications to the builder will be reflected in the final collaborator record. This is different from `CollaboratorAccountReady`, which fires after the account is fully set up. If you need to react to a completed registration rather than modify one in progress, that event is the right choice. For the full lifecycle of system events and how they connect to the rest of Siren's architecture, see the [System Events overview](/documentation/developer-reference/events-system). ## Commerce Events (Extension Development) Source: https://www.sirenaffiliates.com/documentation/extensions/commerce-events The domain events your extension's transformers produce — constructor signatures and extension-specific guidance. # Commerce Events Commerce events are the domain events your extension produces when something happens in the integrated platform. This includes sales, refunds, and coupon applications. Your transformer returns one of these event instances (or `null` to skip), and Siren's core handles everything downstream. See [Event Bindings and Transformers](/documentation/extensions/event-bindings-and-transformers) for how to wire these into your Integration class. All commerce events live in `Siren\Commerce\Events\` and implement [PHPNomad's `Event` interface](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Extensions never dispatch these events directly. They return them from transformer callbacks, and the framework broadcasts them. ## What happens when a sale is detected? `SaleTriggered` is the primary entry point for the entire conversion pipeline. When your transformer determines that an order should be tracked, it returns a `SaleTriggered` instance carrying the opportunity, the line items, and a reference back to the external order. ```php return new SaleTriggered( $opportunity->getId(), // which affiliate referral led to this sale $transactionDetails, // line items from your adapter (array of detail arrays) 'wc', // your extension's ID (matches Integration::getId()) $orderId, // external order ID for mapping back to the platform 'wc_order' // external type identifier for the mapping table ); ``` The `$transactionDetails` array comes from your adapter's `toArray()` method. Each element represents a line item with a name, description, type, per-unit value in cents, quantity, and currency code. See the [Adapters](/documentation/extensions/adapters) guide for the full format. Once `SaleTriggered` broadcasts, Siren's core creates the [transaction](/documentation/resource-reference/transactions), evaluates which programs apply, builds [conversion](/documentation/resource-reference/conversions) records, and generates [obligations](/documentation/resource-reference/obligations). Your extension's job ends at producing the event. See the [SaleTriggered](/documentation/developer-reference/events-commerce/sale-triggered) event reference for full payload and listener details. The transformer is responsible for three checks before returning this event: locating the affiliate opportunity for the customer, verifying the order hasn't already been processed (via the mapping table), and confirming the order has line items. If any check fails, return `null`. ## How does Siren know when payment is confirmed? `TransactionCompleted` fires when an order reaches its final approved state. This typically happens when a payment gateway confirms the charge. This is separate from `SaleTriggered` because many platforms create orders before payment clears. ```php return new TransactionCompleted($transaction); ``` The transformer for this event is simpler than the sale transformer. It looks up the existing Siren transaction by querying the mapping table for the external order ID, then wraps it in the event. If no mapping exists (the order was never tracked by Siren), return `null`. When this event fires, Siren marks associated conversions as approved and advances obligations from draft to pending status, making them eligible for payout. See the [TransactionCompleted](/documentation/developer-reference/events-commerce/transaction-completed) event reference for full details. ## What triggers a refund? `RefundTriggered` fires when a completed order is reversed. This could be an explicit refund, a cancellation, or an admin trashing the order. ```php return new RefundTriggered($transaction); ``` Like `TransactionCompleted`, the transformer looks up the Siren transaction via the mapping table. If no mapping exists, the order was never tracked, so there's nothing to refund. Return `null`. When this event fires, Siren cancels all conversions tied to the transaction, marks the transaction as refunded, and adjusts incentive and distribution metrics. See the [RefundTriggered](/documentation/developer-reference/events-commerce/refund-triggered) event reference for full details. You typically bind this to multiple platform hooks to cover all the ways an order can be reversed: ```php RefundTriggered::class => [ ['action' => 'woocommerce_order_status_completed_to_failed', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_completed_to_cancelled', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_completed_to_refunded', 'transformer' => $refundCallback], ['action' => 'wc-completed_to_trash', 'transformer' => $refundCallback], ], ``` ## How do coupon codes create engagements? `CouponApplied` fires when a customer uses a coupon code during checkout. This event exists specifically to create an engagement that links the coupon code back to the collaborator who owns it. ```php return new CouponApplied(strtoupper($couponCode), $opportunity); ``` Two things to note: coupon codes are always uppercased for consistent matching, and this transformer uses `CurrentUserOpportunity` locators instead of `VisitorOpportunity` because the coupon is applied by the logged-in customer during checkout, not tied to a specific order. When this event fires, Siren's `BoundCouponUsed` engagement trigger strategy looks up the collaborator from the coupon code via the alias table, then creates engagements for every active program that collaborator participates in with the `boundCouponUsed` trigger enabled. See the [CouponApplied](/documentation/developer-reference/events-commerce/coupon-applied) event reference for full details. ## How are subscription renewals tracked? `RenewalTriggered` fires when a subscription renewal payment completes. This is the most complex commerce event because it needs to link back to the original transaction so commissions continue on recurring revenue. ```php return new RenewalTriggered( $originalTransaction, // the Siren transaction from the initial purchase $transactionDetails, // line items for the renewal order $orderId, // external renewal order ID 'wc_order' // external type ); ``` The transformer must trace the renewal order back to the original subscription's first order, then look up the Siren transaction for that original order via the mapping table. It also performs the standard duplicate check on the renewal order ID. Renewal support is conditional. WooCommerce only adds the binding when WooCommerce Subscriptions is installed: ```php if (class_exists(WC_Subscription::class)) { $triggers[RenewalTriggered::class] = [ ['action' => 'woocommerce_subscription_renewal_payment_complete', 'transformer' => $renewalCallback], ]; } ``` See the [RenewalTriggered](/documentation/developer-reference/events-commerce/renewal-triggered) event reference for full details. ## What about leads and form submissions? `LeadTriggered` is the non-monetary equivalent of `SaleTriggered`. It fires when a form submission or signup action occurs, carrying an opportunity ID and source identifier but no transaction details (leads have no monetary value at trigger time). Form-based integrations like Gravity Forms use this event instead of `SaleTriggered`. The downstream pipeline creates conversions and obligations using the lead incentive type rather than sale-based incentive types. See the [LeadTriggered](/documentation/developer-reference/events-commerce/lead-triggered) event reference for full details. For the full lifecycle of a tracked sale — from hook to conversion to payout — see the [Architecture Overview](/documentation/extensions/architecture-overview). For complete event payload documentation, listener details, and the execution order across all event types, see the [Events Reference](/documentation/developer-reference/events-introduction). ## Commerce Events (Reference) Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce Domain events triggered by e-commerce activity. Sales, refunds, coupons, renewals, and leads. import CodeTabs from "@/components/content/CodeTabs.astro"; import EventFlow from "@/components/content/EventFlow.astro"; # Commerce Events Commerce events are the entry point into Siren's attribution pipeline. They fire when something happens in an integrated e-commerce platform: a sale completes, a coupon is applied, a subscription renews, or a form submission arrives. All commerce events live in the `Siren\Commerce\Events` namespace and implement PHPNomad's `Event` interface. See the [integration feature matrix](/documentation/general/integration-feature-matrix) for which events each integration supports. Extensions produce commerce events through transformers. Your extension code does not dispatch these events directly; it returns an event instance from a transformer callback, and the framework broadcasts it. See the [Commerce Events extension guide](/documentation/extensions/commerce-events) for how to produce these events from your own integration. ## How commerce events enter the pipeline Commerce events follow a consistent path from the e-commerce platform into Siren's core. An extension registers a transformer that maps a platform hook (such as a WooCommerce order status change) to a commerce event class. When the hook fires, the framework calls the transformer, which constructs the appropriate event instance and returns it. The framework then broadcasts the event through the event bus, where core listeners pick it up and begin processing. This design means extensions never call the event dispatcher themselves. They declare what platform activity maps to which event, and the framework handles the rest. The result is a clean boundary between platform-specific integration code and Siren's core attribution logic. Each event carries the data that downstream listeners need: opportunity IDs for attribution lookup, transaction details for conversion building, binding fields for mapping back to external records, and source identifiers for tracking which extension produced the event. The typical sale flow looks like this: For coupon-based attribution, the flow is slightly different. The coupon event fires first to establish the engagement, then a subsequent sale event creates the conversion: ## Commerce events in this category ### [SaleTriggered](/documentation/developer-reference/events-commerce/sale-triggered) The primary commerce event for purchases. Fires when an extension detects an order that Siren should track. This event carries the opportunity ID, transaction details, and source identifier, and triggers the conversion pipeline that produces conversions, obligations, and eventually payouts. ### [TransactionCompleted](/documentation/developer-reference/events-commerce/transaction-completed) Fires when an order reaches its final approved state, typically after the payment gateway confirms the charge. This is separate from `SaleTriggered` because many platforms create orders before payment clears. Approving a transaction advances its obligations from draft to pending status, making them eligible for payout. ### [RefundTriggered](/documentation/developer-reference/events-commerce/refund-triggered) Fires when a completed order is reversed, whether through a cancellation, refund, or deletion. Core listeners cancel the associated conversions, mark the transaction as refunded, and adjust metric totals so collaborator performance numbers stay accurate. ### [CouponApplied](/documentation/developer-reference/events-commerce/coupon-applied) Fires when a customer uses a coupon code during checkout. Unlike sale events, this event carries the coupon code and the opportunity rather than transaction details. Siren uses the coupon code to look up which collaborator owns it and creates engagements that a subsequent sale event converts into credited commissions. ### [RenewalTriggered](/documentation/developer-reference/events-commerce/renewal-triggered) Fires when a subscription renewal payment completes. This event traces back to the original purchase transaction so that the same collaborator continues earning commissions on recurring revenue. The conversion process mirrors the initial sale flow but uses the original transaction's attribution data instead of looking up a new opportunity. ### [LeadTriggered](/documentation/developer-reference/events-commerce/lead-triggered) The non-monetary equivalent of `SaleTriggered`. Fires when a form submission or signup action occurs, carrying an opportunity ID and source string but no transaction details. The downstream pipeline creates conversions and obligations using lead-specific incentive types rather than sale-based ones. ## Listening to commerce events from an extension Here is a complete example of registering a listener that reacts to `SaleTriggered` from an extension. The listener uses dependency injection to access the collaborator datastore and a logger. ```php */ class NotifyOnSale implements CanHandle { protected CollaboratorDatastore $collaborators; protected LoggerStrategy $logger; public function __construct( CollaboratorDatastore $collaborators, LoggerStrategy $logger ) { $this->collaborators = $collaborators; $this->logger = $logger; } public function handle(Event $event): void { $this->logger->info( 'Sale triggered for opportunity ' . $event->getOpportunityId() . ' from source ' . $event->getSource() ); } } ``` ```php use PHPNomad\Events\Interfaces\HasListeners; use Siren\Commerce\Events\SaleTriggered; use Siren\WordPress\Extensions\MyExtension\Listeners\NotifyOnSale; class MyExtensionInitializer implements HasListeners { public function getListeners(): array { return [ SaleTriggered::class => NotifyOnSale::class, ]; } } ``` The listener is resolved through the DI container when the event fires, so `CollaboratorDatastore` and `LoggerStrategy` are injected automatically. See the [Listeners & Event Handlers](/documentation/extensions/listeners) guide for more patterns and details on writing listener classes. ## Configs Source: https://www.sirenaffiliates.com/documentation/resource-reference/configs Accessing and querying Siren's key-value configuration system and incentive configs through the PHP data layer. import CodeTabs from "@/components/content/CodeTabs.astro"; # Configs Siren's configuration system stores key-value settings using a compound key of type, subtype, and key. This three-level hierarchy lets different parts of the system namespace their settings without collision. A program's incentive settings, a distributor's schedule parameters, and a global feature flag can all coexist in the same table with different type and subtype values. The config system is designed for settings that need to persist across requests and survive plugin updates. Unlike WordPress options or transients, config values are scoped to Siren's own table and travel with the Siren data layer regardless of platform. > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Most configuration is written through Siren's admin UI, which fires the appropriate events. Direct config writes are appropriate for programmatic setup (recipes, migrations, custom initialization code) and for reading config values in your own event handlers. ## Accessing config data The config datastore is available through dependency injection or the static facade. ```php use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore; class FeatureChecker { protected ConfigDatastore $configs; public function __construct(ConfigDatastore $configs) { $this->configs = $configs; } public function isFeatureEnabled(string $feature): bool { return (bool) $this->configs->getConfigValue('features', 'global', $feature, false); } } ``` ```php use Siren\Configs\Core\Facades\Configs; $isEnabled = (bool) Configs::getConfigValue('features', 'global', 'advancedReporting', false); ``` ## The config model Each config record is represented by a `Config` model instance. | Field | Type | Description | |---|---|---| | `type` | string | Top-level category (e.g., `features`, `incentive`, `distribution`) | | `subtype` | string | Second-level grouping (e.g., `global`, a program ID, a distributor ID) | | `key` | string | Specific setting name within the type/subtype scope | | `value` | mixed | Stored configuration value, which can be any serializable type | Getter methods: `getType()`, `getSubtype()`, `getKey()`, `getValue()`. The identity of a config record is the compound key of type, subtype, and key together. There is no single auto-incrementing primary key. The `getIdentity()` method returns an array with `type`, `subtype`, and `configKey`. ## Available methods The config datastore provides the standard CRUD methods documented in the [introduction](/documentation/resource-reference/introduction). It adds these domain-specific methods. ### Getting a config object `getConfig` retrieves a full `Config` model for a specific type/subtype/key combination. If the config doesn't exist, it returns a `Config` instance initialized with the provided default value rather than throwing an exception. This makes it safe to call without wrapping in a try/catch. ```php use Siren\Configs\Core\Facades\Configs; // Returns a Config model -- never throws for missing configs $config = Configs::getConfig('distribution', 'schedule', 'frequency', 'monthly'); echo $config->getValue(); // 'monthly' if not set, or the stored value ``` ### Getting a config value directly `getConfigValue` is a convenience method that returns just the value rather than the full model. This is the most common accessor when you only need the setting value. ```php use Siren\Configs\Core\Facades\Configs; $frequency = Configs::getConfigValue('distribution', 'schedule', 'frequency', 'monthly'); $threshold = Configs::getConfigValue('fulfillment', 'rules', 'minimumPayout', 0); ``` ### Setting a config value `setConfig` creates or updates a configuration value. If a record with the given type/subtype/key already exists, it is updated in place. Otherwise, a new record is created. This method is available on the datastore interface through dependency injection. ```php use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore; // Inside a service class with ConfigDatastore injected $this->configs->setConfig('distribution', 'schedule', 'frequency', 'weekly'); $this->configs->setConfig('fulfillment', 'rules', 'minimumPayout', 5000); ``` ### Getting all configs in a group `getConfigGroup` retrieves all config records that share a type and subtype. This is useful for loading an entire settings panel at once. ```php use Siren\Configs\Core\Facades\Configs; $scheduleSettings = Configs::getConfigGroup('distribution', 'schedule'); foreach ($scheduleSettings as $config) { echo $config->getKey() . ': ' . $config->getValue(); } ``` ### Deleting a config `deleteConfig` removes a specific configuration entry by its compound key. This method is available on the datastore interface through dependency injection. ```php use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore; // Inside a service class with ConfigDatastore injected $this->configs->deleteConfig('distribution', 'schedule', 'frequency'); ``` ## Incentive configs Siren provides a specialized interface for managing program-specific incentive configuration through the Incentives facade. Under the hood, incentive configs use the same config table with the type set to `incentive` and the subtype set to the program ID. The Incentives facade wraps this pattern so you don't need to manually construct the type/subtype values. ### Accessing incentive configs ```php use Siren\Incentives\Core\Interfaces\IncentiveConfigDatastore; class IncentiveSettings { protected IncentiveConfigDatastore $incentiveConfigs; public function __construct(IncentiveConfigDatastore $incentiveConfigs) { $this->incentiveConfigs = $incentiveConfigs; } public function getCommissionRate(int $programId): float { return (float) $this->incentiveConfigs->getConfig($programId, 'commissionRate', 0.10); } public function setCommissionRate(int $programId, float $rate): void { $this->incentiveConfigs->setConfig($programId, 'commissionRate', $rate); } } ``` ```php use Siren\Incentives\Core\Facades\Incentives; $rate = Incentives::getConfig($programId, 'commissionRate', 0.10); Incentives::setConfig($programId, 'commissionRate', 0.15); ``` ### Getting an incentive config value `getConfig` on the Incentives facade retrieves a configuration value for a specific program and key. The program ID replaces the type/subtype pair from the general config system. You only need the program ID and key. ```php use Siren\Incentives\Core\Facades\Incentives; $cookieLifetime = Incentives::getConfig($programId, 'cookieLifetime', 30); $autoApprove = Incentives::getConfig($programId, 'autoApprove', false); ``` ### Setting an incentive config value `setConfig` creates or updates an incentive configuration for a program. The method returns the datastore instance for fluent chaining. ```php use Siren\Incentives\Core\Facades\Incentives; Incentives::setConfig($programId, 'commissionRate', 0.20); // Fluent chaining through the datastore Incentives::setConfig($programId, 'cookieLifetime', 60) ->setConfig($programId, 'autoApprove', true); ``` ### Getting all configs for a program `getConfigs` returns all incentive configuration records for a program as an array of `Config` models. ```php use Siren\Incentives\Core\Facades\Incentives; $allSettings = Incentives::getConfigs($programId); foreach ($allSettings as $config) { echo $config->getKey() . ': ' . $config->getValue(); } ``` ### Deleting an incentive config `deleteConfig` removes a specific incentive configuration for a program. ```php use Siren\Incentives\Core\Facades\Incentives; Incentives::deleteConfig($programId, 'cookieLifetime'); ``` ## Configuration Source: https://www.sirenaffiliates.com/documentation/extensions/wp-configuration How get_option, update_option, and post meta translate to Siren's three-level config system. import CodeTabs from "@/components/content/CodeTabs.astro"; # Configuration WordPress stores settings with `get_option` / `update_option` (global settings) and `get_post_meta` / `update_post_meta` (per-entity settings). Siren replaces both with a unified config system that uses a three-level hierarchy: type, subtype, and key. ```php // WordPress: global settings update_option('my_plugin_api_key', 'abc123'); $key = get_option('my_plugin_api_key'); // WordPress: per-entity settings update_post_meta($post_id, '_commission_rate', '15'); $rate = get_post_meta($post_id, '_commission_rate', true); ``` ```php // Siren facade: three-level config hierarchy use Siren\Configs\Core\Facades\Configs; // Read a config value: type, subtype, key, default $rate = Configs::getConfigValue('program', '42', 'maxCommission', '0'); // Write a config value: type, subtype, key, value Configs::getContainedInstance()->setConfig('program', '42', 'maxCommission', '15'); ``` ```php // Siren DI: inject ConfigDatastore use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore; class CommissionService { protected ConfigDatastore $config; public function __construct(ConfigDatastore $config) { $this->config = $config; } public function getMaxCommission(int $programId): string { return $this->config->getConfigValue( 'program', (string) $programId, 'maxCommission', '0' // default if not set ); } public function setMaxCommission(int $programId, string $value): void { $this->config->setConfig( 'program', (string) $programId, 'maxCommission', $value ); } } ``` ## How does the three-level hierarchy work? Every config value is scoped by three coordinates: - `type` is the domain area. Examples: `'program'`, `'collaborator'`, `'system'`. - `subtype` is the specific entity or context within that type. For entity-scoped settings, this is typically the entity's ID as a string (e.g., `'42'`). For global settings within a domain, this might be a category name. - `key` is the individual setting name. Examples: `'maxCommission'`, `'payoutThreshold'`, `'notificationEmail'`. This maps cleanly to both of WordPress's storage patterns. Global settings (`get_option`) correspond to a fixed type and subtype with varying keys. Per-entity settings (`get_post_meta`) correspond to a fixed type and key with varying subtypes (one per entity ID). ```php // Global setting: same type and subtype, different keys $this->config->getConfigValue('system', 'general', 'siteName', ''); $this->config->getConfigValue('system', 'general', 'currency', 'USD'); // Per-program setting: same type and key, different subtypes (program IDs) $this->config->getConfigValue('program', '42', 'maxCommission', '0'); $this->config->getConfigValue('program', '99', 'maxCommission', '0'); ``` ## Why a single system instead of two? WordPress's split between `wp_options` and `wp_postmeta` creates a conceptual divide. Global settings live in one place, per-entity settings in another, and the APIs are different. Siren's config system is one table, one API, one mental model. The scope is determined by the type/subtype/key coordinates, not by which function you call. This also means config values are queryable the same way regardless of scope. You can fetch all config values for a given type and subtype, or all values with a given key across all subtypes. ## Where can I learn more? For the full `ConfigDatastore` API (querying, deleting, and batch operations), see the [Configs](/documentation/resource-reference/configs) resource reference. For the full PHPNomad framework documentation on datastores, see [Datastores Introduction](https://phpnomad.com/core-concepts/datastores/introduction). ## Configure cascade payouts Source: https://www.sirenaffiliates.com/documentation/getting-started/configure-cascade-payouts Bind a collaborator group to a program or distributor, pick a cascade calculation strategy, and see per-layer payouts fire. import StepList from "@/components/content/StepList.astro"; import Screenshot from "@/components/content/Screenshot.astro"; You already have a CollaboratorGroup with a `linearChain` or `parentChild` structure. This tutorial wires it into a [program](/documentation/general/what-are-programs) so a single sale pays out across multiple layers of the chain or tree. By the end you'll have triggered a real engagement, watched Siren produce one credit per layer, and seen those layered credits land on the right collaborators inside the program's obligations. ## Before you start You need a few things in place before the walkthrough makes sense: - Siren Pro is active on the site. - A CollaboratorGroup with structure `linearChain` or `parentChild`, already saved. If you haven't built one, follow [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group) first. - A program you can edit. A fresh test program is fine, pick one you don't mind reconfiguring. - At least one engagement type the program uses (Sales, Manual, Coupon Code Used, whatever you plan to trigger). The same flow works on distributors too (the last section covers that), but the main walkthrough is on the program side because most operators start there. ## Step 1: Bind the program to your collaborator group The cascade needs a group to walk. That binding lives on the program edit screen, in the Collaborators section. {/* Screenshot needed: program edit screen, Collaborators section, Group tab active with a linearChain group selected from the dropdown. */} That's all the binding takes. The interesting part is what it unlocks in the next step. ## Step 2: Pick a cascade calculation method Reopen the program for editing. Find the engagement type whose calc you want to change. Sales is the most common, but Manual works just as well if you want to test by hand. Each engagement type has its own calculation dropdown. Before the binding, that dropdown only offered Fixed. With a `linearChain` or `parentChild` group bound, two new options appear: - [Upline cascade](/documentation/calculation-strategies/upline-cascade): credit collaborators above the trigger. - [Downline cascade](/documentation/calculation-strategies/downline-cascade): credit collaborators below the trigger. Pick Upline cascade for this walkthrough. The dropdown swaps in a different set of inputs. {/* Screenshot needed: engagement type calc dropdown opened, showing Fixed / Upline cascade / Downline cascade, with Upline cascade highlighted. */} If you don't see Upline or Downline in the dropdown, the bound group is probably flat. Flat groups don't expose the `hasLayer` capability that cascade calcs require, so the picker hides them. Read [Calc capability matching](/documentation/general/calc-capability-matching) for the full mechanic, then re-check your group's structure. ## Step 3: Set per-layer points Fixed asks for a single value. Upline and Downline replace that with five inputs: `pointsAtLayer1` through `pointsAtLayer5`. Each layer is one step away from the triggering collaborator. Layer 1 is directly above (for upline) or directly below (for downline). Layer 5 is the furthest the cascade will reach. For a three-tier payout, set: - `pointsAtLayer1`: 100 - `pointsAtLayer2`: 50 - `pointsAtLayer3`: 25 - `pointsAtLayer4`: 0 - `pointsAtLayer5`: 0 A zero (or negative) on any layer stops the cascade right there. Layer 4 is zero, so the walk stops after layer 3, even if the chain or tree extends further. Five layers is the hard cap. You can't extend past it from the UI. Save the program. {/* Screenshot needed: the pointsAtLayer1..pointsAtLayer5 inputs filled with 100, 50, 25, 0, 0. */} ## Step 4: Fire a trigger and see the cascade Now trigger the engagement type you configured. For Sales, place a WooCommerce order that is attributed to one of the collaborators in the group. Attribution is what ties the order to a collaborator, usually their referral link or their coupon code, so check out using that collaborator's link or coupon. Pick a collaborator who sits deep enough in the chain (or tree) to have real upline above them. For Manual, create a manual engagement on that collaborator directly, with no order needed. Once the trigger fires, open the program's engagements list. You'll see multiple rows appear for that single sale, one per layer that earned a credit. With a chain of five and a sale triggered by the bottom collaborator with per-layer config `100, 50, 25, 0, 0`: - The collaborator one step above the seller gets a row with 100 points. - The collaborator two steps above gets a row with 50 points. - The collaborator three steps above gets a row with 25 points. - Nothing for layer 4 and layer 5. The cascade stopped at the first zero. - The collaborator who actually triggered the sale gets no row. Cascades never credit the trigger, only the layers above (or below) them. If anyone in the chain is suspended or deleted, that layer is skipped and pays nothing. The credit is not reassigned, and the cascade does not renumber. Everyone else keeps their own layer and rate. So if the layer-1 person is suspended, layer 1 pays out nothing and the layer-2 person still earns 50, the layer-2 rate, not 100. {/* Screenshot needed: program engagements list filtered to the test sale, showing three rows with point values 100, 50, 25 attributed to three different collaborators. */} ## Step 5: Watch the obligations The engagements are only half the picture. When the program's incentive structure resolves (which happens when the transaction completes), those per-layer point values drive the actual dollar split. If you're using [Performance-weighted pool](/documentation/distribution-structures/performance-weighted-pool) (the natural fit for cascades), the pool divides the commission proportionally to each collaborator's score. Scores of 100, 50, and 25 split the pool 4:2:1. On a $70 commission pool that's $40 to layer 1, $20 to layer 2, and $10 to layer 3. Open the obligations list for the program and you'll see those dollar amounts land on the same three collaborators that earned the engagement rows in the previous step. Same people, same proportions, just now expressed in money rather than points. {/* Screenshot needed: program obligations list filtered to the test transaction, showing $40 / $20 / $10 obligations against the three credited collaborators. */} If you swap the program over to a different distribution structure (say, top score wins), the layer-1 collaborator takes the whole pool because they have the highest score. The cascade just produces the scores. What happens with them is the distribution structure's job. ## The same flow on a distributor [Distributors](/documentation/general/what-are-distributors) work the same way. The Distributors edit screen has the same Group tab on its Collaborators section, the same calc picker on each metric type, the same per-layer inputs when you pick Upline or Downline cascade. The only difference is that you're configuring a metric type instead of an engagement type, and you'll trigger a metric instead of a sale. Bind the group, pick the cascade calc on the metric type, set per-layer points, fire the metric trigger. You'll see the same multi-row pattern in the distributor's metric list, one row per credited layer, the triggering collaborator excluded. ## What you've done You bound a collaborator group to a program, picked a cascade calculation strategy, configured per-layer points, fired a real trigger, and watched Siren produce a multi-layer payout that flowed all the way through to dollar obligations. A few places to go next: - [What is a cascade](/documentation/general/what-is-a-cascade): the concept page, if you want the mental model laid out cleanly now that you've seen it work. - [Upline cascade](/documentation/calculation-strategies/upline-cascade) and [Downline cascade](/documentation/calculation-strategies/downline-cascade): per-strategy reference for the exact behavior and edge cases. - [Choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy): when to pick Fixed vs Upline vs Downline. - [Linear chain](/documentation/collaborator-group-structures/linear-chain) and [Parent-child](/documentation/collaborator-group-structures/parent-child): the two structures that unlock cascades, in detail. ## Connect Beacon to ChatGPT Source: https://www.sirenaffiliates.com/documentation/beacon/chatgpt Set up Beacon MCP on ChatGPT using developer mode. import StepList from "@/components/content/StepList.astro"; If you just want a quick way to use Beacon with ChatGPT, the [Siren Affiliates Beacon custom GPT](https://chatgpt.com/g/g-69d6578e07bc8191b5e0820b489f6446-siren-affiliates-beacon) works immediately with no setup. The MCP integration below is for users who want the full programmatic connection. ChatGPT supports MCP servers through its developer mode feature. This is available to Plus, Pro, Business, Enterprise, and Education accounts. ## Enable Developer Mode ## Create a Beacon App ## Use Beacon in a Conversation ChatGPT will ask you to confirm tool calls before executing them. You can review the details of each call and approve or deny it. ## Connect Beacon to Claude Source: https://www.sirenaffiliates.com/documentation/beacon/claude Set up Beacon MCP on Claude.ai and Claude Code CLI. import StepList from "@/components/content/StepList.astro"; ## Claude.ai Beacon's tools will now be available in your Claude conversations. ## Claude Code (CLI) Add Beacon to your Claude Code settings. For global access across all projects, edit `~/.claude/settings.json`: ```json { "mcpServers": { "beacon": { "type": "url", "url": "https://beacon.sirenaffiliates.com/beacon/v1/mcp" } } } ``` For project-scoped access, add the same configuration to `.claude/settings.json` in your project root. ## Connect Beacon to Code Editors Source: https://www.sirenaffiliates.com/documentation/beacon/code-editors Configuration methods for connecting Beacon MCP to VS Code, JetBrains IDEs, Cursor, and other MCP-compatible editors. Most code editors with MCP support accept server configuration in one of two formats: an HTTP URL or a JSON configuration block. Beacon supports HTTP connections. ## Server URL If your editor has an "Add MCP server" dialog that accepts a URL directly, use: ``` https://beacon.sirenaffiliates.com/beacon/v1/mcp ``` ## HTTP JSON configuration If your editor expects a JSON configuration block (common in settings files), use: ```json { "beacon": { "type": "url", "url": "https://beacon.sirenaffiliates.com/beacon/v1/mcp" } } ``` Where this JSON goes depends on the editor. Some use a dedicated MCP settings panel, others expect it in a project or user settings file. ## Editor-specific documentation Each editor handles MCP configuration differently. Refer to the vendor documentation for where to add the configuration: - **VS Code** (Claude Code extension): [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code/mcp) - **JetBrains IDEs** (AI Assistant plugin): Settings > Tools > AI Assistant > Model Context Protocol - **Cursor**: [Cursor MCP documentation](https://docs.cursor.com/context/model-context-protocol) The configuration above works with any editor that supports MCP over HTTP. ## Conversion Events Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions Domain events for conversion processing, approval, rejection, and renewal. # Conversion Events Conversion events drive the core of Siren's attribution pipeline. They cover the full lifecycle of a conversion, from the initial trigger through award, approval or rejection, and renewal. These events are defined in `Siren\Conversions\Core\Events` and are the primary mechanism through which the system creates conversions, issues obligations, and handles recurring payments. ## The creation flow The conversion creation flow begins with `ConversionInitialized` and ends with `ConversionsAwarded`. These two events represent the entry and exit of the `BuildConversions` listener, which is responsible for matching an opportunity to qualifying programs and creating conversion records. [`ConversionInitialized`](/documentation/developer-reference/events-conversions/conversion-initialized) is the main entry point for the conversion pipeline. It fires when a commerce event is detected and an opportunity exists to attribute it. The event carries opportunity, transaction, and optional external binding data that the `BuildConversions` listener uses to evaluate qualifying programs. [`ConversionsAwarded`](/documentation/developer-reference/events-conversions/conversions-awarded) fires after `BuildConversions` has successfully created conversion records for a specific program. It carries the full incentive context, including the conversions themselves, the matching program, and the resolver used to calculate rewards. This is the branching point for obligation creation, engagement completion, and other downstream effects. ## The review flow After conversions are created, they go through a review process. The two events here represent the two possible outcomes of that review. [`ConversionApproved`](/documentation/developer-reference/events-conversions/conversion-approved) fires when a conversion passes review, either through auto-approval or manual admin action. Its primary downstream effect is transitioning the conversion's associated obligation from draft to pending status, making it eligible for the next fulfillment run. [`ConversionRejected`](/documentation/developer-reference/events-conversions/conversion-rejected) fires when a conversion is denied, typically because of a refund or manual rejection by an admin. It triggers obligation cleanup for the rejected conversion. ## Renewal events Renewal events handle recurring payments such as subscription renewals. They parallel the creation flow but start from existing engagements rather than live opportunities. [`ConversionRenewed`](/documentation/developer-reference/events-conversions/conversion-renewed) fires when a subscription renewal payment is detected. Unlike `ConversionInitialized`, which works from an opportunity, this event works from the original engagement records created during the initial sale. The engagement reuse preserves the original attribution, so the collaborator who was credited with the initial sale continues to receive credit for renewals without needing a new opportunity or engagement cycle. After renewal conversions are built, a `RenewedConversionsAwarded` event fires with the same shape as `ConversionsAwarded`, carrying the full incentive context for downstream listeners. ## Program group resolution [`ProgramGroupConversionTriggered`](/documentation/developer-reference/events-conversions/program-group-conversion-triggered) fires when a conversion involves programs that belong to a program group. Program groups enforce mutual exclusivity: only one program in the group can award a conversion for a given opportunity. The event identifies the winning program (determined by the group's resolution strategy) and the losing programs, allowing listeners to clean up engagements on the losers and ensure only the winner proceeds through the conversion flow. ## Manual attribution [`ManualAttributionRequested`](/documentation/developer-reference/events-conversions/manual-attribution-requested) fires when an admin manually attributes a conversion to a specific collaborator. This bypasses the normal engagement-based attribution flow entirely. The event provides the collaborator, transaction, and opportunity needed to create conversions without relying on tracked engagements. See [Manually Attribute a Transaction](/documentation/getting-started/manually-attribute-a-transaction) for a user-level overview of when and how to use this feature. ## ConversionApproved Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/conversion-approved Fires when a conversion passes review through auto-approval or manual admin action. # ConversionApproved Conversions do not immediately result in payouts. They go through a review step first, either automated or manual. When a conversion passes that review, the system fires `ConversionApproved`, signaling that the conversion is legitimate and its associated obligations should move forward. The event is identified as `conversion_approved` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The event carries a single `Conversion` model representing the conversion that was approved. ```php use Siren\Conversions\Core\Events\ConversionApproved; public function handle(Event $event): void { $conversion = $event->getConversion(); } ``` ## What happens when it fires? The primary downstream listener is `MarkObligationsAsPending`. When a conversion is approved, this listener transitions the conversion's associated obligation from draft to pending status. The distinction between draft and pending is significant: draft obligations are invisible to the fulfillment system, while pending obligations become eligible for inclusion in the next fulfillment run. This two-step process (create as draft, then promote to pending on approval) gives admins a window to review conversions before they become financial commitments. Auto-approval rules can skip this window for trusted conversion types, but the event fires in both cases. See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## ConversionInitialized Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/conversion-initialized The main entry point for the conversion pipeline. Fires when a commerce event is detected and an opportunity exists to attribute it. # ConversionInitialized The conversion pipeline begins here. When a commerce event is detected and an opportunity exists to attribute it, the system fires `ConversionInitialized`. This is the moment where a tracked customer interaction becomes a potential conversion, kicking off the evaluation of programs, incentive rules, and ultimately the creation of conversion records. The event is identified as `conversion_initialized` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The event provides the `opportunityId` that links the conversion back to the customer's tracked session, a `conversionType` string that identifies what kind of outcome occurred (a sale, a lead, a subscription payment), and transaction data describing the financial details. The transaction data can be either a `Transaction` model (if one was already created) or an array of transaction detail arrays that the pipeline will use to create one. Optional binding fields allow the conversion to be linked to an external record in another system. A WooCommerce order ID or an EDD payment ID, for example, would be passed through `bindingId` and `bindingDataType`. ```php use Siren\Conversions\Core\Events\ConversionInitialized; public function handle(Event $event): void { $opportunityId = $event->getOpportunityId(); $type = $event->getConversionType(); // Transaction data may be a model or a raw details array $transaction = $event->getTransaction(); $details = $event->getTransactionDetails(); // Optional external binding $bindingId = $event->getBindingId(); $bindingDataType = $event->getBindingDataType(); } ``` ## What happens when it fires? The `BuildConversions` listener picks up this event. It queries for all active programs that match the opportunity's engagements, evaluates each program's incentive rules against the transaction data, and creates conversion records for every qualifying program. Once conversions are built for a program, the listener broadcasts `ConversionsAwarded` for that program, handing off to the next stage of the pipeline. This separation matters because it keeps the detection of commerce events completely decoupled from the logic of evaluating programs. The commerce integration only needs to fire `ConversionInitialized` with the right data. Everything else is handled downstream. See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## ConversionRejected Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/conversion-rejected Fires when a conversion is denied due to refund or manual rejection. # ConversionRejected Not every conversion survives review. When a customer requests a refund or an admin manually rejects a conversion, the system fires `ConversionRejected`. This event triggers the cleanup of any obligations that were created during the conversion flow. The event is identified as `conversion_rejected` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The event carries a single `Conversion` model representing the conversion that was rejected. ```php use Siren\Conversions\Core\Events\ConversionRejected; public function handle(Event $event): void { $conversion = $event->getConversion(); } ``` ## What happens when it fires? The `RejectObligations` listener handles the obligation cleanup. Any obligations associated with the rejected conversion are updated to reflect that they will not be fulfilled. This prevents rejected conversions from appearing in future fulfillment runs and ensures collaborators are not paid for transactions that were reversed or deemed invalid. The rejection flow is the mirror image of approval. Where `ConversionApproved` promotes obligations from draft to pending, `ConversionRejected` ensures those obligations never reach the fulfillment pipeline. See [How Refunds Work](/documentation/general/how-refunds-work) for the full chain from refund through conversion rejection to obligation cleanup. See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## ConversionRenewed Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/conversion-renewed Fires when a subscription renewal payment is detected. Works from original engagements rather than live opportunities. # ConversionRenewed Subscription businesses need renewals to carry the same attribution as the original sale. When a subscription renewal payment is detected, the system fires `ConversionRenewed` instead of `ConversionInitialized`. The difference is fundamental: while `ConversionInitialized` starts from a live opportunity, `ConversionRenewed` starts from the original engagement records that were created during the initial sale. The event is identified as `conversion_renewed` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The event carries the array of original `Engagement` records from the initial sale, a `Transaction` model for the renewal payment, a `conversionType` string, and optional binding information (`bindingId` and `bindingDataType`) for linking to external records. ```php use Siren\Conversions\Core\Events\ConversionRenewed; public function handle(Event $event): void { $originalEngagements = $event->getOriginalEngagements(); $transaction = $event->getTransaction(); $type = $event->getConversionType(); $bindingId = $event->getBindingId(); $bindingDataType = $event->getBindingDataType(); } ``` ## Why reuse the original engagements? The engagement reuse preserves the original attribution chain. The collaborator who was credited with the initial sale continues to earn on renewals without needing a new opportunity or engagement cycle. This is the correct behavior for subscription programs: a customer who signed up through an affiliate link six months ago should still generate commissions for that affiliate when their subscription renews. ## What happens downstream? After renewal conversions are built, the system fires `RenewedConversionsAwarded` rather than `ConversionsAwarded`. `RenewedConversionsAwarded` is the renewal counterpart to `ConversionsAwarded` and carries the same shape of data: an array of `Conversion` models, the original engagements, the `Transaction`, the `Incentive` and `IncentiveResolver`, the `Program`, and the `conversionType`. Downstream listeners that handle obligation creation and engagement management listen to both events. See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## Conversions Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions Conversion records in Siren's attribution pipeline — data model, status lifecycle, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; # Conversions A conversion represents a tracked event where a [collaborator](/documentation/resource-reference/collaborators)'s [engagement](/documentation/resource-reference/engagements) resulted in a measurable outcome — a sale, a lead capture, or a subscription renewal. Conversions sit between engagements (the upstream referral activity) and [obligations](/documentation/resource-reference/obligations) (the downstream reward owed to the collaborator). They are the pivot point in Siren's attribution pipeline: an engagement produces a conversion, and an approved conversion triggers an obligation. Conversions are created by Siren's event pipeline when an engagement is matched to a qualifying outcome. The incentive system evaluates the program's rules, checks the engagement bindings, and creates conversion records automatically. The most common reasons to access conversion data directly are reading conversions for dashboards or reports, looking up conversions to make decisions in custom logic, and writing migration scripts that import historical data from another system. ## The conversion object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Engagement | `engagementId` | `getEngagementId()` | integer | The engagement that produced this conversion | | Transaction | `transactionId` | `getTransactionId()` | integer or null | The associated transaction (e.g., an order), if any | | Obligation | `obligationId` | `getObligationId()` | integer or null | The obligation created from this conversion, if any | | Type | `type` | `getType()` | string | Conversion type identifier (e.g., `sale`, `lead`, `renewal`) | | Status | `status` | `getStatus()` | string | Current status (see lifecycle below) | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the record was created | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the record was last updated | The `transactionId` and `obligationId` fields are nullable because not every conversion starts with both. A conversion is created when the outcome is detected, but the obligation is only linked later once the incentive system calculates what the collaborator is owed. Similarly, some conversion types (like lead captures) may not involve a transaction at all. ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested. | Field | Type | Description | |---|---|---| | `engagementScore` | mixed | Score from the originating engagement | | `engagementStatus` | string | Status of the originating engagement | | `programId` | integer | ID of the program the conversion belongs to | | `collaboratorId` | integer | ID of the collaborator who earned this conversion | | `collaboratorName` | string | Display name of the collaborator | | `transactionStatus` | string | Status of the linked transaction | | `transactionTotal` | mixed | Total value of the linked transaction | ## The ConversionType object Conversion types define what kinds of events can be tracked as conversions. Each type declares which incentive structures support it. | Field | Type | Description | |---|---|---| | `id` | string | Unique type identifier (e.g., `sale`, `lead`) | | `label` | string | Plural display label | | `singularLabel` | string | Singular display label | | `supportedIncentives` | string[] | Array of incentive type IDs this conversion type supports | ## Status lifecycle | Status | Description | |---|---| | `pending` | Initial state, awaiting review. Every conversion starts here. | | `approved` | The conversion is valid and triggers downstream obligation creation through the ConversionApproved event and MarkObligationsAsPending listener. This is the path that ultimately leads to a collaborator being rewarded. | | `rejected` | The conversion was denied. Fires the ConversionRejected event, which triggers RejectObligations to cancel any linked obligation. | | `expired` | The conversion fell outside the attribution window and is no longer valid. Expiration logic depends on program configuration. | | `deleted` | Soft-deleted. A second DELETE call permanently removes the record from the database. | See [How Refunds Work](/documentation/general/how-refunds-work) for a complete walkthrough of how refunds propagate through the system. ## Accessing conversion data ```bash # List pending conversions for a collaborator curl -X GET "https://your-site.com/wp-json/siren/v1/conversions?status=pending&fields=id,type,status,collaboratorName" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single conversion with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/conversions/15?fields=id,type,status,programId,collaboratorName,transactionTotal" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Conversions\Core\Datastores\Conversion\Interfaces\ConversionDatastore; class ConversionReport { protected ConversionDatastore $conversions; public function __construct(ConversionDatastore $conversions) { $this->conversions = $conversions; } public function getPendingConversions(): array { return $this->conversions->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); } } ``` ```php use Siren\Conversions\Core\Facades\Conversions; $pending = Conversions::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); $conversion = Conversions::getById(42); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Creating conversions manually via PHP bypasses the attribution system. The engagement won't be validated, the program's incentive rules won't be evaluated, and downstream obligations won't be created through the normal flow. If you need to trigger a conversion, fire the appropriate domain event instead of writing directly to this datastore. ## PHP domain methods The conversion datastore supports all shared methods documented in the [introduction](/documentation/resource-reference/introduction). There are no additional domain-specific methods beyond the standard CRUD operations. ### Querying conversions for a transaction A common read pattern is finding all conversions tied to a specific transaction. This is how Siren's own listeners determine which conversions to approve or reject when a transaction completes or is refunded. ```php $conversions = $this->conversions->andWhere([ ['column' => 'transactionId', 'operator' => '=', 'value' => $transactionId] ]); ``` ### Querying conversions by engagement To find conversions that originated from a specific engagement: ```php $conversions = $this->conversions->andWhere([ ['column' => 'engagementId', 'operator' => '=', 'value' => $engagementId] ]); ``` ### Counting pending conversions ```php $pendingCount = $this->conversions->countAndWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); ``` ## Relationships Every conversion is linked to exactly one [engagement](/documentation/resource-reference/engagements) via `engagementId`. The engagement sits upstream in the attribution pipeline and must exist at creation time, validated by the `IdsExist` middleware. A conversion may optionally reference a [transaction](/documentation/resource-reference/transactions) (such as an e-commerce order) via `transactionId`. This links the conversion to the concrete event it represents. On the downstream side, approving a conversion causes the system to create an [obligation](/documentation/resource-reference/obligations) recording what is owed to the [collaborator](/documentation/resource-reference/collaborators). The `obligationId` back-reference is then stored on the conversion, closing the loop between the referral activity and the reward. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## ConversionsAwarded Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/conversions-awarded Fires after BuildConversions has created conversion records for a specific program. The branching point for obligation creation. # ConversionsAwarded After the `BuildConversions` listener finishes creating conversion records for a program, it fires `ConversionsAwarded`. This event marks the transition from "conversions exist" to "the system can act on them." It carries everything downstream listeners need to create obligations, update engagement states, and calculate rewards. The event is identified as `conversions_awarded` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The payload is rich by design. It includes the array of `Conversion` models that were created, the `Transaction` (if any), the `Incentive` type and `IncentiveResolver` that were used to calculate rewards, the `Program` that matched, the `opportunityId`, and the `conversionType`. This breadth of context means listeners never need to re-query for information that was already resolved during the build phase. ```php use Siren\Conversions\Core\Events\ConversionsAwarded; public function handle(Event $event): void { $conversions = $event->getConversions(); $program = $event->getProgram(); $transaction = $event->getTransaction(); $incentive = $event->getIncentiveType(); $resolver = $event->getIncentiveResolver(); $opportunityId = $event->getOpportunityId(); $conversionType = $event->getConversionType(); } ``` ## Why is this a branching point? Several listeners respond to this event, each handling a different concern. `CreateObligationForConversion` uses the incentive context to create obligation records that describe what the collaborator is owed. `MarkEngagementsComplete` transitions the opportunity's engagements from active to complete, signaling that the attribution cycle for those engagements has concluded. Because the event carries the full incentive context, any custom listener that needs to know how rewards were calculated can inspect the `Incentive` and `IncentiveResolver` directly from the event rather than re-running the incentive evaluation logic. See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## Cookie Duration and Attribution Windows Source: https://www.sirenaffiliates.com/documentation/general/cookie-duration How Siren tracks visitors over time, how long the attribution window stays open, and how this interacts with refunds and renewals. An attribution window is the amount of time Siren keeps a visitor connected to the [collaborator](/documentation/general/what-is-a-collaborator) who referred them. If a visitor clicks an affiliate link today and buys a week later, whether the affiliate earns credit depends on whether the attribution window is still open when the purchase happens. Every affiliate platform has some version of this mechanic. Siren's version is configured per program, and it's one of the first settings you'll want to think through when setting up a new [program](/documentation/general/what-are-programs). This page explains what the window is, how Siren stores the connection between the visitor and the collaborator, how long the window lasts, and what happens when it closes. If you're coming from Refersion, Commission Junction, Impact, Post Affiliate Pro, or any other tracking platform, the underlying mechanic is the same. Only the settings are named differently. ## How Siren stores the attribution When a visitor clicks a referral link or applies a coupon code, Siren creates an [opportunity](/documentation/general/what-is-an-opportunity) that represents the visitor session. The opportunity is tied to the collaborator through an [engagement](/documentation/general/what-is-an-engagement), which is the per-program credit claim for that visit. A cookie in the visitor's browser keeps the connection alive across sessions, so when they come back a day or a week later, Siren can still tell which collaborator they arrived through. The cookie doesn't do the attribution on its own. It's the lookup key that tells Siren which opportunity to load. The opportunity holds the actual connection to the collaborator and the engagement. This split matters because it means the attribution survives even if the visitor clears other cookies, closes the browser, or comes back from a different device (as long as they return through the same browser that holds the Siren cookie). ## How long the window lasts Each program in Siren has an expiration time, measured in days, that controls how long the attribution window stays open for that program. You configure it when you create the program, and you can change it later from the program edit screen. There's no global default that applies to all programs. Each program sets its own window independently, because different programs usually have different sales cycles. Practical guidance on common window lengths: Thirty days is a common starting point for most traditional affiliate programs. It's long enough to cover the typical consideration period for a consumer purchase, short enough that it doesn't artificially inflate attribution for purchases the affiliate wasn't really responsible for. Seven to fourteen days fits programs that want to reward last-click behavior, where the goal is to credit the referral that actually led to the conversion rather than a click from weeks ago. This is common for impulse-purchase products or for programs where the seller wants attribution to feel current. Sixty to ninety days fits high-consideration purchases (expensive products, long sales cycles, B2B software trials) where the customer realistically needs time to decide. Pushing the window shorter than the actual consideration period means affiliates stop earning for referrals they legitimately drove. None of these numbers are prescriptive. Think about how long it typically takes a referred visitor to convert, then pick a window that covers that time with a bit of buffer. ## What happens when the window closes When the window closes, the opportunity expires. Subsequent purchases by the same visitor don't credit the original collaborator unless a new engagement is recorded. If the visitor clicks the same affiliate's link again after the window expired, a new opportunity and a new engagement get created, and the clock starts over. But if they just come back directly and buy, the original collaborator earns nothing because the attribution has timed out. This is intentional and matches how cookie-based attribution works across every affiliate platform. A cookie without an expiration would mean a single click could claim attribution for every purchase that visitor ever makes, which isn't the incentive structure most programs want. ## Interactions with refunds and renewals Refunds aren't affected by the attribution window. When a refund happens, the original conversion is reversed regardless of whether the cookie has expired, because the refund is tied to the transaction itself rather than to a live cookie. A customer who buys today, gets attributed to their affiliate, and returns the product in six months will have their conversion reversed through the normal [refund pipeline](/documentation/general/how-refunds-work). The affiliate's original credit goes away because the sale itself was undone, not because the window closed. Renewals on subscription products work differently. If your store uses subscriptions and you've enabled renewal tracking, each renewal fires a new conversion tied to the renewal event. The attribution window behavior then depends on whether the renewal tracking is turned on and whether your integration supports it. See the [integration feature matrix](/documentation/general/integration-feature-matrix) for which integrations support renewals. For SaaS programs with long trial-to-paid windows, the relevant question is whether your attribution window is long enough to span the trial. If a customer signs up for a 30-day free trial through an affiliate link and your attribution window is 14 days, the trial will convert to paid after the window closes and the affiliate will get no credit. For that situation, set the window to match or exceed your trial length plus a buffer. When the subscription programs doc ships at [subscription programs](/documentation/getting-started/subscription-programs), it'll cover this scenario in more detail. ## Related reading - [What is an Opportunity?](/documentation/general/what-is-an-opportunity) for the underlying data model - [What is an Engagement?](/documentation/general/what-is-an-engagement) for how per-program credit works - [How Refunds Work](/documentation/general/how-refunds-work) for the interaction between refunds and attribution - [How to spot and prevent affiliate fraud](/blog/how-to-spot-and-prevent-affiliate-fraud) for why shortening the window is one of the fixes for coupon-stacking abuse ## Coupon Code Tracking Source: https://www.sirenaffiliates.com/documentation/general/coupon-tracking How to use coupon codes for affiliate tracking in Siren: assigning codes to collaborators, how attribution works, and which integrations support it. import StepList from "@/components/content/StepList.astro"; Coupon code tracking lets collaborators promote your products using a discount code instead of (or in addition to) a referral link. When a customer enters a coupon code at checkout, Siren attributes the sale to the collaborator who owns that code. This is useful in situations where a clickable link isn't practical, like podcast sponsorships, printed materials, or social media posts where the audience is more likely to remember a code than click a URL. ## How it works Siren doesn't create or manage coupon codes in your commerce plugin. You create the coupon in WooCommerce, Easy Digital Downloads, LifterLMS, or NorthCommerce the way you normally would, setting whatever discount amount, expiration, and usage limits you need. Then you assign that coupon to a collaborator through a field that Siren adds to the coupon edit screen. When a customer applies the code at checkout, Siren detects it and creates an [engagement](/documentation/general/what-is-an-engagement) for the collaborator who owns that code. From there, the normal attribution pipeline takes over. If the customer completes the purchase, a [conversion](/documentation/general/what-is-a-conversion) is created and an [obligation](/documentation/general/what-are-obligations) records what's owed. The coupon code is stored as an [alias](/documentation/resource-reference/aliases) with the type set to "coupon." This means the same historical tracking that applies to referral codes applies to coupon codes. If you reassign a code to a different collaborator, the old assignment is preserved so that historical conversions stay attributed correctly. ## Assigning a coupon to a collaborator The process is the same across all supported integrations. You can see all coupon assignments for a specific collaborator on their profile in the Siren admin. The coupons tab lists every coupon code assigned to them along with the date it was assigned. ## Which integrations support coupon tracking Not every integration handles coupon detection the same way. See the [integration feature matrix](/documentation/general/integration-feature-matrix) for a full comparison. WooCommerce, Easy Digital Downloads, and NorthCommerce all detect coupon usage at the moment the code is applied during checkout. This means the engagement is created as soon as the customer enters the code, before the order is placed. LifterLMS handles it differently. LifterLMS doesn't provide a hook for when a coupon is applied during checkout, so Siren detects the coupon at the time the sale completes instead. The end result is the same (the collaborator gets credit), but the timing is slightly different. Gravity Forms and LearnDash do not support coupon tracking because they don't have a coupon system. ## Coupon tracking alongside referral links A customer might arrive through a referral link and also enter a coupon code at checkout. When this happens, both the referral link visit and the coupon usage create separate engagements. If those engagements belong to the same collaborator across different [programs](/documentation/general/what-are-programs), both programs can fire independently. If they belong to the same program or to programs in the same [program group](/documentation/general/what-are-program-groups), the group's sorter determines which engagement wins. This means coupon tracking doesn't replace referral link tracking. They work together. A collaborator can share both a link and a code, and Siren tracks both interactions. The coupon code engagement trigger requires at least one active integration that supports coupon detection for it to appear in program settings. ## Coupon Code Used Source: https://www.sirenaffiliates.com/documentation/general/coupon-code-used When a customer uses a collaborator's coupon code at checkout, the Coupon Code Used event is triggered. import EventFlow from "@/components/content/EventFlow.astro"; When a customer applies a coupon code assigned to a [collaborator](/documentation/general/what-is-a-collaborator) during checkout, the "Coupon Code Used" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the purchase to the collaborator who owns that code. ## Why this trigger usually wins Coupons get applied at the very end of the buying process, which means the Coupon Code Used event typically fires after every other engagement for the same customer. When a [program](/documentation/general/what-are-programs) uses a "newest engagement wins" sorter, the coupon owner usually gets the conversion. That makes this trigger the anchor for most [coupon-based influencer programs](/recipes/coupon-based-influencer-program), where the collaborator's entire contribution is tied to whether their code gets used. ## Which integrations support it WooCommerce, Easy Digital Downloads, and NorthCommerce all detect the coupon the moment it's applied at checkout. LifterLMS works a little differently because it doesn't expose a hook for coupon application. Siren detects the coupon at the time the sale completes instead, so the engagement is created alongside the conversion rather than before it. Either way, the collaborator gets credit. For the full assignment workflow and a per-integration breakdown, see [coupon code tracking](/documentation/general/coupon-tracking). ## CouponApplied Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce/coupon-applied Fires when a customer uses a coupon code during checkout. Powers coupon-based affiliate attribution. # CouponApplied `CouponApplied` fires when a customer uses a coupon code during checkout. This is how Siren powers coupon-based affiliate attribution: the coupon event creates the engagement that links the customer to a collaborator, and a subsequent `SaleTriggered` event creates the conversion that credits them. The event ID is `coupon_used`, and its fully qualified class is `Siren\Commerce\Events\CouponApplied`. ## What does this event carry? Unlike the sale events, `CouponApplied` does not carry transaction details. It carries two things: the coupon code as a string (always uppercased for consistent matching against Siren's alias table) and the `Opportunity` model for the customer session. ```php use Siren\Commerce\Events\CouponApplied; $event = new CouponApplied(strtoupper($couponCode), $opportunity); ``` ## How does the pipeline react? The `BoundCouponUsed` engagement trigger strategy handles this event. It looks up which collaborator owns the coupon code (via the [alias system](/documentation/resource-reference/aliases)) and creates engagements for every active program where that collaborator has the coupon trigger enabled. See [Coupon Code Tracking](/documentation/general/coupon-tracking) for a user-level overview of how coupon attribution works across integrations. The coupon event and the sale event work in sequence. The coupon event establishes attribution by creating the engagement. A subsequent `SaleTriggered` event, which fires when the checkout completes, creates the conversion that actually credits the collaborator with the sale. For the full lifecycle of commerce events and how they connect to the rest of the attribution pipeline, see the [Commerce Events overview](/documentation/developer-reference/events-commerce). ## Course Completed Source: https://www.sirenaffiliates.com/documentation/general/course-completed When a student completes a course owned by a collaborator, the Course Completed event is triggered. import EventFlow from "@/components/content/EventFlow.astro"; When a student finishes a course that's owned by a [collaborator](/documentation/general/what-is-a-collaborator), the "Course Completed" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the completion to the instructor who owns the course. ## How it's typically used Course Completed is built for sites that host multiple instructors and want to pay those instructors based on how much their material is actually getting used. You handle the platform, the marketing, and the student experience, and the instructors get credited whenever their work reaches the finish line. It's common to pair this trigger with [Lesson Completed](/documentation/general/lesson-completed) so the instructor earns a smaller amount per lesson and a larger amount when a student reaches the end of the course. The [instructor revenue share](/recipes/instructor-revenue-share) recipe is a ready-to-use example of that pattern. ## Integration requirements Course Completed requires either the LifterLMS or LearnDash integration. Siren listens for the completion event from whichever LMS you're using and attributes it to the course owner set in your course settings. ## Create a collaborator group Source: https://www.sirenaffiliates.com/documentation/getting-started/create-a-collaborator-group Walk through creating a collaborator group, picking a structure, and adding members from the admin UI. import StepList from "@/components/content/StepList.astro"; import Screenshot from "@/components/content/Screenshot.astro"; A [collaborator group](/documentation/general/what-are-collaborator-groups) bundles a set of collaborators into a single unit that programs and distributors can bind to. By the end of this tutorial you'll have a saved group, a chosen structure, and a roster of members, ready to wire into a program or a distributor. ## Before you start You need Siren Plus or higher to create flat collaborator groups. Linear chain and parent-child structures need Siren Pro. The screens look the same in both tiers. Pro just adds the two cascading structures to the dropdown. You also need a few [collaborators](/documentation/general/what-is-a-collaborator) already in the system, since you can't add what doesn't exist yet. If you're starting from an empty install, create three or four collaborators first (see [managing collaborators](/documentation/getting-started/managing-collaborators-affiliates)) so you have something to drag around. ## Step 1: Open the Collaborator Groups screen The Collaborator Groups list lives under the Siren menu. It shows every group on the site, the structure each one uses, and the member count. If this is your first group, the list is empty and the only thing on the screen is the Add Group button. ## Step 2: Create the group Click Add Group. The create screen asks for a name, an optional description, and a structure. The structure is the most consequential choice. It decides how cascades walk the group and which calc strategies the picker shows you later. The three structures behave differently. Flat treats every member as a peer, so there is no upline, no downline, and no cascade. [Linear chain](/documentation/collaborator-group-structures/linear-chain) orders members top-to-bottom so that cascades walk up or down the chain, while [parent-child](/documentation/collaborator-group-structures/parent-child) lets you build a tree where cascades walk along the parent or child branches instead. If you're not sure which fits, the [structure comparison page](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) walks through the trade-offs. You can change a group's structure later if you start with the wrong one, but the per-member ordering does not carry over. A flat group switched to a linear chain or parent-child starts with every member unordered, so you would re-set each member's position or parent before a cascade reads it the way you intend. If you already know you will need cascades, picking the right structure now saves that rework. ## Step 3: Add members Once the group is saved, the edit screen shows an empty members table and an Add Members button. The picker opens in a modal so you can browse, search, and stage a batch before committing. Staging is deliberate. Until you click Save, nothing is persisted. The staged list is a preview of what will change. This lets you stack several edits (add some members, reorder them, set a parent) and commit them as one save. If you navigate away with pending changes, the UI warns you first. ## Step 4: Arrange members (chain or tree) If you picked Flat, skip this step. Members have no order in a flat group. If you picked Linear Chain or Parent-Child, the members table includes drag handles and the parent dropdown so you can shape the group. ### Linear chain: drag to reorder Each row in a linear chain group has a drag handle on the left. Grab a row and drag it up or down. A drop indicator shows where the row will land when you release. Position 1 sits at the top of the chain and is the most upline. The bottom row is the most downline. The position values are recomputed on save. You don't pick the numbers. Siren assigns them based on the visual order. If you drag chain-three above chain-two, chain-three becomes position 2 and chain-two becomes position 3. ### Parent-child: drag to reparent + dropdown fallback Parent-child rows have a drag handle too, but they respond to both vertical and horizontal motion. Vertical drag places the row next to a different sibling. Horizontal drag changes the indent depth. Drag right to nest under the row above, drag left to outdent and become a sibling of the current parent. The same row also has a Parent dropdown. The dropdown is the deterministic path: pick a parent from the list and the tree updates without any drag math. Use the dropdown when you want a screen-reader-friendly path, or when the drag delta isn't reading the depth you wanted. Siren prevents cycles. If you try to set a collaborator's parent to one of its own descendants, the dropdown grays that option out and the drag refuses to land. ## What you've done You created a collaborator group, picked its structure, added members, and (if you picked linear chain or parent-child) arranged them into the shape you want. The group is now a reusable unit any program or distributor can bind to. The next step is wiring the group into a payout. Head to [configure cascade payouts](/documentation/getting-started/configure-cascade-payouts) to bind this group to a program and pick a cascade calc that matches its structure. ## Create a Program in Siren Source: https://www.sirenaffiliates.com/documentation/getting-started/create-a-program-in-siren Step-by-step instructions on how to create your first program using Siren. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; import StepList from "@/components/content/StepList.astro"; > **Prefer AI-guided setup?** [Beacon](/integrations/mcp-server/) is our free AI assistant that can design and generate a complete program configuration for you. Just describe what you want and it'll create a recipe you can install with one click. [Connect Beacon](/documentation/beacon/introduction) to get started. > **Just want the fastest path?** The [quick start](/documentation/getting-started/quick-start) guide installs a recipe in about ten minutes and skips most of the configuration walkthrough below. ## What is a program? A [program](/documentation/general/what-are-programs) is the rule set that defines how collaborators earn rewards. It controls which actions get tracked, how commissions are calculated, and which parts of a transaction count toward the payout. Every incentive you run in Siren starts here. To create one, hover over Siren in the WordPress sidebar, click Programs, then click Add New. ## Basic settings Give your program a name and optional description. A handy convention is including the commission rate in the name (like "Affiliate Program 15%") so you can tell programs apart at a glance. Set the status to Active and pick your payout currency. If you've already created a [program group](/documentation/general/what-are-program-groups), assign this program to it. If not, leave it as "No Group" and revisit after reading about [program groups](/documentation/getting-started/multiple-affiliate-programs-using-sirens-program-groups). ## Expiration time The expiration time (in days) is how long a [collaborator's](/documentation/general/what-is-a-collaborator) engagement stays valid. If the customer doesn't convert inside the window, the collaborator won't get credit. Thirty days is a reasonable starting point for a standard affiliate program. A shorter window like seven days creates a conversion-focused incentive. You can run two programs together: a high-commission short-window program for conversions and a lower-commission long-window program for lead generation. ## Program structure The [program structure](/documentation/general/what-are-programs) decides which collaborator wins when a conversion happens. Siren offers five: - **[Newest engagement wins](/documentation/program-structures/top-score-wins)** rewards the most recent engagement. This is the "last click wins" model most affiliate programs use, and it's the right default if you're unsure. It also handles coupon codes naturally. - **[Oldest engagement wins](/documentation/program-group-structures/oldest-engagement-wins)** does the opposite and rewards whoever first brought the customer in. - **Shared engagement pool** splits the commission evenly among every collaborator who engaged the customer. - **[Performance weighted pool](/documentation/program-structures/performance-weighted-pool)** splits by points rather than equal shares, rewarding sustained effort. - **[Top score wins](/documentation/program-structures/top-score-wins)** pays the full commission to whoever accumulated the most points. For most affiliate programs, pick newest engagement wins. If you want a single sale to credit a collaborator's upline or downline across multiple layers (a tiered or sales-override structure), bind this program to a [collaborator group](/documentation/general/what-are-collaborator-groups) on the program's Group tab and use a [cascade calculation strategy](/documentation/general/what-is-a-cascade). See [Configure cascade payouts](/documentation/getting-started/configure-cascade-payouts) for the full walkthrough. ## Engagement tracking events [Engagement tracking events](/documentation/general/what-is-an-engagement) are the actions Siren measures on the path to a conversion. Enable the ones that matter for your program: - **[Site visited](/documentation/general/site-visited)** fires when a customer arrives through an affiliate link. Standard affiliate tracking. - **[Coupon code used](/documentation/general/coupon-code-used)** fires when a customer applies a coupon assigned to a collaborator. See [Coupon Code Tracking](/documentation/general/coupon-tracking) for integration support. - **[Blog post visited](/documentation/general/blog-post-visited)** fires when a customer reads a post authored by a collaborator. Useful for content-driven programs. - **[Collaborator product sold](/documentation/general/collaborator-product-sold)** fires when a product owned by a collaborator is purchased. The foundation of royalty programs. - **[Course completed](/documentation/general/course-completed)** and **[lesson completed](/documentation/general/lesson-completed)** are LMS-specific and reward instructors when students finish their material. For a simple affiliate program, enable site visited and coupon code used. Point values only matter if you're using a point-based structure like performance weighted pool or top score wins. ## Incentive structure The incentive structure decides how rewards are calculated: - **[Percentage of transaction](/documentation/incentive-structures/percentage-of-transaction)** pays a percentage of the transaction total. Standard for affiliate programs. - **[Fixed per transaction](/documentation/incentive-structures/fixed-per-transaction)** pays a flat amount per sale regardless of value. Common for high-ticket products or referral bonuses, like hosting companies paying $200 per signup. - **[Fixed per product](/documentation/incentive-structures/fixed-per-product)** pays a flat amount for each qualifying product in the cart. Useful for targeted promotions on specific inventory. For percentage-based programs, you can credit conversions for sales, renewals, or both. Sales covers initial purchases. Renewals covers recurring subscription charges. Whether to include renewals depends on whether this program rewards acquisition or retention. You can always create a separate renewal-focused program later. Auto-approving completed transactions approves [obligations](/documentation/general/what-are-obligations) the moment an order completes. Leave it unchecked if you'd rather review each one manually. ## Commission pool settings These control which parts of a transaction feed the commission calculation. **Line items** should almost always be checked. They're the actual products purchased. **Discounts** should usually be checked too. When checked, Siren subtracts discounts before calculating. A $120 product with a $20 coupon calculates on $100 rather than $120. **Fees** covers things like WooCommerce signup fees or subscription fees. Include them if they're part of what you want to pay commission on. **Shipping** and **taxes** should almost never be checked. You don't want to pay commission on tax owed to the government or pass-through shipping costs. When you check line items, filtering options appear. You can restrict the program to specific product categories (by slug), SKUs, or product types like subscriptions versus simple products. There's also a collaborator-owned filter for royalty programs. See [Line Item Filters](/documentation/general/line-item-filters) for a full reference. Once everything is set, click Create. Your program is live and ready for [collaborators](/documentation/getting-started/managing-collaborators-affiliates) to start earning. The video walks through creating a simple affiliate program from start to finish. It covers naming the program (with a tip about including the commission rate in the name), setting status and currency, and the expiration time window that controls how long a collaborator's engagement stays valid. From there it explores the five program structures, recommending newest engagement wins as the right default for most affiliate programs. It then walks through the available engagement tracking events, enabling site visited and coupon code used for the simple case, and explains when you'd reach for blog post visited, collaborator product sold, or the LMS-specific events. Finally, it configures a percentage-of-transaction incentive at 15%, credits conversions for sales only, leaves auto-approve on, and sets the commission pool to include line items, discounts, and fees while leaving shipping and taxes off. The video closes by clicking Create and confirming the new program appears in the list. ## Create a Revenue Share in Siren Source: https://www.sirenaffiliates.com/documentation/getting-started/create-a-revenue-share-in-siren Step-by-step walkthrough for setting up a revenue share program in Siren — paying creators, vendors, or partners a share of the sales they drive. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; import StepList from "@/components/content/StepList.astro"; > **Prefer AI-guided setup?** [Beacon](/integrations/mcp-server/) is our free AI assistant that can design and generate a complete revenue share configuration for you. Just describe what you want and it'll create a recipe you can install with one click. [Connect Beacon](/documentation/beacon/introduction) to get started. > **Prerequisite:** This guide assumes you're already comfortable creating a program. If not, start with [Create a Program in Siren](/documentation/getting-started/create-a-program-in-siren) first. ## Programs vs. distributors [Programs](/documentation/general/what-are-programs) pay per transaction. When a sale happens, the collaborator who referred it gets a reward right then. That's a great fit for affiliate-style incentives, but it falls apart when you need to reward cumulative contribution over time. [Distributors](/documentation/general/what-are-distributors) fill that gap. A distributor pools revenue from qualifying [transactions](/documentation/general/what-are-transactions) across a period, tracks collaborator performance, then splits the pool on a schedule. It's the difference between a per-deal commission and a monthly performance bonus. Distributors power revenue shares, bonus pools, and any incentive where collaborators are compared against each other before anyone gets paid. Not sure which structure fits your program? See [Choosing a Distribution Structure](/documentation/distribution-structures/choosing-a-distribution-structure). ## A course creator revenue share Picture an education site that sells access through memberships instead of per-course purchases. Multiple creators produce content, and you want to pay each one a share of monthly membership revenue based on how much students engage with their courses. A standard program can't do this. There's no single transaction tied to a specific creator. Instead, you set a revenue percentage (say, 25% of membership revenue goes to the creator pool), track engagement metrics over the month, and let Siren split the pool based on performance. ## Configure the distributor ### Revenue percentage This is the size of the pool, not a per-collaborator rate. Set it to 25% and 25% of all qualifying revenue during the period goes into the pool. Earn $10,000 in memberships, and $2,500 is available to split. ### Distribution schedule Choose weekly, monthly, or annual. Monthly is the most common for revenue shares. When the scheduled date hits, Siren tallies metrics and revenue, calculates each collaborator's share, and creates [obligations](/documentation/general/what-are-obligations) automatically. ### Metric tracking events Metrics define what you're measuring to determine performance. They work like [engagement](/documentation/general/what-is-an-engagement) tracking events in programs, but they award points instead of triggering payouts. Each metric has a point value. For a course platform you might set course completed to 10 points and lesson completed to 1 point, so a creator whose students finish whole courses earns far more credit than one whose students only click a lesson or two. Available metrics include [site visited](/documentation/general/site-visited), [blog post visited](/documentation/general/blog-post-visited), [collaborator product sold](/documentation/general/collaborator-product-sold), [coupon code used](/documentation/general/coupon-code-used), [course completed](/documentation/general/course-completed), and [lesson completed](/documentation/general/lesson-completed). ### Incentive structure The incentive structure decides how the pool is divided. - [Shared engagement pool](/documentation/program-structures/shared-engagement-pool) splits evenly among any collaborator who earned at least one point. - [Performance weighted pool](/documentation/program-structures/performance-weighted-pool) splits proportionally based on scores. This is the usual pick for revenue shares. - [Top score wins](/documentation/program-structures/top-score-wins) gives the whole pool to the top scorer. Use this for competitive bonus programs. ### Commission pool filters These filters control which parts of a transaction count toward the pool. You can include or exclude discounts, fees, shipping, taxes, and specific line items. For a membership revenue share you'd typically restrict line items to subscriptions (excluding one-time purchases), subtract discounts (so you're not paying on revenue you never collected), and leave shipping and taxes off. See [Line Item Filters](/documentation/general/line-item-filters) for the full reference. ## What happens when distribution triggers While the period is active, Siren quietly tracks every qualifying metric event and transaction. Points accumulate for each collaborator. Revenue accumulates in the pool. On the scheduled date, Siren calculates each collaborator's share using the incentive structure you picked, then creates obligations. You review and approve them through the normal [fulfillment](/documentation/general/what-is-a-fulfillment) process. The next period resets and the cycle starts over. Distributors track collaborator contributions over time and reward them on a schedule instead of per transaction. That makes them the right tool for revenue shares and bonus programs where you need to compare collaborators against each other before paying anyone. The walkthrough uses a course-creator revenue share as the example. The revenue percentage sets the size of the pool (not a per-collaborator rate), the distribution schedule controls when Siren runs the payout, and metric tracking events award points based on what students do, so creators whose students finish whole courses earn a bigger share than creators whose students only dabble. An incentive structure (shared pool, performance weighted, or top score wins) decides how the pool is split, and commission pool filters decide which parts of each transaction count toward the pool. Once the distributor is active, Siren handles the tracking and creates obligations automatically when the period closes. ## Create Collaborator Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/create Create a new collaborator record with program enrollment. ### Create Collaborator `POST /siren/v1/collaborators` Creates a new collaborator record. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `fullName` | string | Yes | The collaborator's full name | | `nickname` | string | Yes | Display name | | `email` | string | Yes | Email address (must be unique within the organization) | | `status` | string | Yes | Initial status: `active`, `inactive`, or `pending` | | `programs` | integer[] | No | Array of program IDs to enroll the collaborator in (all must exist) | **Example Request:** ```json { "fullName": "Jane Smith", "nickname": "Jane", "email": "jane@example.com", "status": "active", "programs": [1, 3] } ``` **Example Response:** ```json { "id": 7, "fullName": "Jane Smith", "nickname": "Jane", "email": "jane@example.com", "status": "active", "createdDate": "2026-04-08T12:00:00Z", "modifiedDate": "2026-04-08T12:00:00Z" } ``` **Events:** Broadcasts `CollaboratorActionEvent` (action: Create) after success. ## Create Collaborator Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/create Creates a new collaborator group with an optional initial set of members. # Create Collaborator Group `POST /siren/v1/collaborator-groups` Creates a new collaborator group. Optionally writes an initial member set at creation time. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name for the group | | `structure` | string | Yes | Registered structure resolver id. Today's values: `flat`, `linearChain`, `parentChild` | | `description` | string | No | Free-form description of the group. Defaults to an empty string when omitted | | `members` | object[] | No | Array of `{ collaboratorId, metadata? }` entries. When provided, the full member set is written via the same replace logic as `PUT /siren/v1/collaborator-groups/{id}/members` | **Example Request:** ```json { "name": "Sales Reps", "description": "Inside sales chain", "structure": "linearChain", "members": [ { "collaboratorId": 41, "metadata": { "position": 1 } }, { "collaboratorId": 42, "metadata": { "position": 2 } } ] } ``` **Example Response:** ```json { "id": 12, "name": "Sales Reps", "description": "Inside sales chain", "structure": "linearChain" } ``` Returns `201` on success with the new group's serialized shape. When `members` is supplied, the rows are written but are not reflected in the response. Use `GET /siren/v1/collaborator-groups/{id}/members?fields=id,collaboratorId,metadata` to confirm them. Responds `500` when the group cannot be created. The create response also includes `dateCreated` and `dateModified` in addition to the four fields shown above. The read endpoints (`GET /collaborator-groups` and `GET /collaborator-groups/{id}`) select fields through the resolver and expose only `id`, `name`, `description`, and `structure`, so those timestamps appear here in the create response but are not retrievable on a later read. **Events:** Broadcasts `CollaboratorGroupCreated`. When an initial `members` set is written, each added row broadcasts a `CollaboratorAddedToCollaboratorGroup` event. ## Create Conversion Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/create Create a new conversion record linked to an engagement. ### Create Conversion `POST /siren/v1/conversions` Creates a new conversion record. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `engagementId` | integer | Yes | ID of the associated engagement (must exist) | | `type` | string | Yes | Conversion type identifier | | `status` | string | Yes | Initial status: `pending`, `approved`, `rejected`, or `expired` | | `transactionId` | integer | No | ID of associated transaction | | `obligationId` | integer | No | ID of associated obligation | #### Example Request ```json { "engagementId": 15, "type": "sale", "status": "pending" } ``` #### Example Response ```json { "id": 43, "engagementId": 15, "transactionId": null, "obligationId": null, "type": "sale", "status": "pending", "dateCreated": "2026-04-06T12:00:00Z", "dateModified": "2026-04-06T12:00:00Z" } ``` #### Events Broadcasts `ConversionActionEvent` (action: Create) after success. ## Create Distributor Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors/create Creates a new distributor with resolver, pool resolver, currency, and optional schedule configuration. # Create Distributor `POST /siren/v1/distributors` Creates a new distributor record. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Distributor name | | `description` | string | Yes | Human-readable description | | `distributionResolver` | string | Yes | Incentive resolver identifier (must be a registered resolver) | | `distributionPoolResolver` | string | Yes | Pool resolver identifier | | `status` | string | Yes | Initial status: `active` or `inactive` | | `units` | string | Yes | Currency identifier (must be a registered currency) | | `schedule` | string[] | No | Array of DateTime modifier strings for distribution scheduling | **Example Request:** ```json { "name": "Monthly Commission", "description": "Monthly percentage-based commission distribution", "distributionResolver": "percentage_based", "distributionPoolResolver": "standard_pool", "status": "active", "units": "USD", "schedule": ["first day of next month"] } ``` **Example Response:** ```json { "id": 2, "name": "Monthly Commission", "description": "Monthly percentage-based commission distribution", "distributionResolver": "percentage_based", "distributionPoolResolver": "standard_pool", "status": "active", "units": "USD", "dateCreated": "2026-04-08T12:00:00Z", "dateModified": "2026-04-08T12:00:00Z" } ``` **Events:** Broadcasts `DistributorActionEvent` (action: Create) after success. ## Create Obligation Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations/create Creates a new obligation record for a collaborator. ### Create Obligation `POST /siren/v1/obligations` Creates a new obligation record. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `collaboratorId` | integer | Yes | ID of the collaborator (must exist) | | `status` | string | Yes | Initial status: `pending`, `fulfilled`, or `cancelled` | | `awardType` | string | Yes | Type of award (e.g., `commission`) | | `value` | integer | Yes | Value in smallest currency unit | | `payoutId` | integer | No | ID of associated payout | #### Middleware `CollaboratorAliasResolverMiddleware` resolves collaborator aliases before the request is processed. #### Example Request ```json { "collaboratorId": 5, "status": "pending", "awardType": "commission", "value": 2500 } ``` #### Example Response ```json { "id": 11, "collaboratorId": 5, "status": "pending", "awardType": "commission", "value": 2500, "payoutId": null, "dateCreated": "2026-04-06T12:00:00Z", "dateModified": "2026-04-06T12:00:00Z" } ``` #### Events Broadcasts `ObligationActionEvent` (action: Create) after success. ## Create Program Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/create Creates a new program with validated incentive type, resolver, currency, and engagement types. # Create Program `POST /siren/v1/programs` Creates a new program. Validates the provided incentive type, incentive resolver type, currency unit, and engagement types against their respective registries before persisting. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name | | `description` | string | Yes | Human-readable description | | `incentiveType` | string | Yes | Must be a registered incentive type | | `incentiveResolverType` | string | Yes | Must be a registered incentive resolver type | | `units` | string | Yes | Must be a registered currency identifier | | `status` | string | Yes | Initial status: `active` or `inactive` | | `engagementTypes` | object | Yes | Map of engagement type keys to their configuration values | **Example Request:** ```json { "name": "Standard Affiliate Program", "description": "Earn commission on every referred sale.", "incentiveType": "commission", "incentiveResolverType": "percentage", "units": "USD", "status": "active", "engagementTypes": { "link_click": { "value": "100" } } } ``` **Example Response:** ```json { "id": 1, "name": "Standard Affiliate Program", "description": "Earn commission on every referred sale.", "incentiveType": "commission", "incentiveResolverType": "percentage", "status": "active", "units": "USD", "dateCreated": "2026-04-08T12:00:00Z", "dateModified": "2026-04-08T12:00:00Z" } ``` **Events:** Broadcasts `ProgramActionEvent` (action: Create) after success. ## Create Program Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups/create Creates a new program group with an optional set of program associations. # Create Program Group `POST /siren/v1/program-groups` Creates a new program group. Optionally associates programs at creation time. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name for the group | | `description` | string | Yes | Description of the group's purpose | | `sorter` | string | Yes | Sorting strategy: `oldestBindingWins` or `newestBindingWins` | | `programs` | integer[] | No | Array of program IDs to associate with the group. IDs must reference existing programs. Programs already assigned to another group are silently skipped. | **Example Request:** ```json { "name": "Seasonal Promotions", "description": "Holiday and seasonal bonus programs that override the default commission.", "sorter": "newestBindingWins", "programs": [3, 7] } ``` **Example Response:** ```json { "id": 2, "name": "Seasonal Promotions", "description": "Holiday and seasonal bonus programs that override the default commission.", "sorter": "newestBindingWins" } ``` Returns `201` on success. The response includes the core fields of the newly created group. Program associations are created but not reflected in the response. Use `GET /siren/v1/program-groups/{id}?fields=id,name,programIds` to confirm them. ## Create Signup Form JWT Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/signup-form-jwt Generate a signed JWT token encoding program enrollment settings for the collaborator signup form. ### Create Signup Form JWT `POST /siren/v1/collaborators/create-signup-form-jwt` Generates a signed JWT token for use with the collaborator signup form. The token encodes program enrollment settings so the public signup endpoint can apply them without exposing admin configuration to the client. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `programIds` | integer[] | Yes | Program IDs that new signups will be enrolled in | | `statusOnSignup` | string | Yes | Status to assign to new collaborators (`active` or `pending`) | | `approveExisting` | boolean | Yes | Whether to auto-approve existing collaborators who sign up again | **Example Response:** ```json { "token": "eyJhbGciOiJIUzI1NiIs..." } ``` ## Create Transaction Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions/create Creates a new transaction with one or more detail line items. ### Create Transaction `POST /siren/v1/transactions` Creates a new transaction with one or more detail line items. The transaction is created with a status of `complete`. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `details` | object[] | Yes | Array of detail line item objects (see below) | | `bindingId` | string or integer | No | External reference ID for mapping to a source system | | `bindingDataType` | string | No | External reference type identifier | Each object in the `details` array accepts: | Field | Type | Required | Description | |---|---|---|---| | `name` | string | Yes | Display name for the line item | | `description` | string | No | Longer description | | `type` | string | No | Type identifier (e.g., `credit`, `debit`) | | `value` | integer | Yes | Value in smallest currency unit (e.g., cents) | | `quantity` | integer | No | Quantity (defaults to 1) | | `units` | string | No | Currency code or unit type (e.g., `USD`) | | `attributes` | object | No | Additional key-value metadata | **Example Request:** ```json { "details": [ { "name": "Manual Credit", "description": "Manual credit for collaborator", "type": "credit", "value": 5000, "quantity": 1, "units": "USD" } ] } ``` **Example Response:** ```json { "id": 102, "status": "complete", "dateCreated": "2026-04-08T12:00:00Z" } ``` Creation may be rejected by event listeners if the details do not pass validation. In that case, the endpoint returns `400` with an error message. ## Creating Transactions Manually Source: https://www.sirenaffiliates.com/documentation/getting-started/creating-transactions-manually How to record a sale or payment in Siren when it happened outside of your normal commerce integration. import StepList from "@/components/content/StepList.astro"; Not every sale flows through your WooCommerce store or your LMS checkout. Phone orders, invoiced deals, offline payments, and sales through channels that Siren doesn't integrate with all need to be recorded if you want collaborators to earn credit for them. The transaction creation screen lets you do that. This guide covers creating the transaction record itself. Once the transaction exists, you can attribute it to a collaborator using the [manual attribution](/documentation/getting-started/manually-attribute-a-transaction) workflow, which runs the transaction through the normal program pipeline and creates conversions and obligations. ## When to create a transaction manually The most common situations are sales that happen outside of your connected commerce plugin. A customer calls in and you process the order manually. A client pays via invoice and the payment never touches your store. A deal closes through a channel that doesn't have a Siren integration. In each case, the sale is real but Siren has no way to detect it automatically. Creating a transaction manually records the financial details of that sale so it can participate in Siren's attribution and commission system. ## How to create a transaction Transactions", description: "Open the Transactions screen in your WordPress admin." }, { title: "Click New Transaction", description: "This opens the transaction creation form." }, { title: "Add your line items", description: "Each line item has a name, type, price, quantity, and currency. Add as many as you need to represent the full sale." }, { title: "Click Create", description: "The transaction is saved with a \"complete\" status, ready for attribution." }, ]} /> Each line item represents a piece of the sale. The fields that matter most are the name (what was sold) and the type, which categorizes the item for [commission calculation](/documentation/general/line-item-filters). The available types are product, subscription, fee, discount, shipping, and tax. If you create a line item with the type "shipping" and the program doesn't include shipping in its calculation, that line item won't count toward the commission. A simple sale might have one product line item. A more complex transaction might include products, a discount, shipping, and tax as separate line items, so that the commission calculation handles them correctly based on the program's compiler settings. ## What happens after you create the transaction Creating a transaction records the sale, but does not create any conversions or commissions. To credit a collaborator, follow the [manual attribution](/documentation/getting-started/manually-attribute-a-transaction) workflow. ## A typical workflow A customer calls to place an order. You process the payment through your payment processor outside of your store. Then you go to Siren > Transactions, create a new transaction with the order details, and save it. Later, when you know which collaborator referred this customer, you select the transaction and attribute it to that collaborator. Siren handles the rest. If you already know who referred the customer at the time you create the transaction, you can create and attribute in quick succession. The two-step process is there for situations where you need to record the sale now and figure out attribution later. ## Credit Collaborator Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions/credit-collaborator Attributes one or more existing transactions to a collaborator through the manual attribution pipeline. ### Credit Collaborator `POST /siren/v1/transactions/credit-collaborator` Attributes one or more existing transactions to a collaborator. For each transaction, the system creates an engagement, conversion, and obligation through the manual attribution pipeline. This is the mechanism for retroactively crediting a collaborator for transactions that were not originally tracked through a referral link. See [Manually Attribute a Transaction](/documentation/getting-started/manually-attribute-a-transaction) for a user-level overview of when and how to use this feature. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `transactionIds` | integer[] | Yes | Array of transaction IDs to credit | | `collaboratorId` | integer | Yes | ID of the collaborator to credit | | `type` | string | No | Conversion type (defaults to `sale`) | The `collaboratorId` field supports collaborator alias resolution. If a collaborator alias is provided, it is automatically resolved to the canonical collaborator ID via middleware. **Example Request:** ```json { "transactionIds": [101, 102, 103], "collaboratorId": 42, "type": "sale" } ``` **Example Response:** ```json { "success": true, "affected": 3 } ``` Non-existent transaction IDs are silently skipped. Datastore errors on individual transactions are logged but do not halt processing of the remaining IDs. **Error Responses:** - `404`. Collaborator not found. - `500`. Error fetching collaborator. ## Credit Metric Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributions/credit-metric Manually credits a metric to a collaborator, triggering the manual metric attribution pipeline across active distributors. # Credit Metric `POST /siren/v1/distributors/credit-metric` Manually credits a metric to a collaborator. This triggers the manual metric attribution pipeline, which creates or updates metric records for every active distributor that has a `manual` metric type configured for the given collaborator. This endpoint lives under `/distributors` rather than `/distributions` because it acts on the distributor configuration layer, not on distribution records directly. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `collaboratorId` | integer | Yes | ID of the collaborator to credit | **Example Request:** ```json { "collaboratorId": 42 } ``` **Example Response:** ```json { "success": true } ``` **Error Responses:** - `404`. Collaborator not found. - `500`. Database error. **Events:** Broadcasts `ManualMetricRequested` after success, which the `Manual` trigger strategy handles to update metric records across applicable distributors. ## Custom calculation strategies Source: https://www.sirenaffiliates.com/documentation/extensions/calculation-strategies Registering a custom engagement-side or metric-side calculation strategy through the calc registry events. A calculation strategy decides who gets credited and how much when an engagement or metric trigger fires. The `Fixed` strategy that ships in the base tier wraps the historical one-credit-at-a-static-value behavior. The [upline cascade](/documentation/calculation-strategies/upline-cascade) and [downline cascade](/documentation/calculation-strategies/downline-cascade) strategies that ship in Pro walk a bound [collaborator group](/documentation/general/what-are-collaborator-groups) and credit peers per layer. You can add your own strategy on either side by listening for a registry event and calling one method. There are two parallel seams, one per side. The engagement side scores program engagements. The metric side scores distributor metrics. They mirror each other in shape, so the pattern below applies to both with the class names swapped. The one thing that is not interchangeable is the context object your `calculate()` reads from, which exposes different fields per side (see below). ## The two registry events Each side broadcasts a registry event once, on first read of its calculation provider service. You register your strategy by listening for that event and calling `addCalculationStrategy()`. Wire the listener through your Initializer's `getListeners()` (shown below) so it is in place during plugin initialization, before that first read. A listener attached after the first read misses the one-time broadcast and your strategy never appears, with no error. If you are new to Siren's event system, see the [events introduction](/documentation/developer-reference/events-introduction) and [listeners](/documentation/extensions/listeners). The engagement-side event is `Siren\Engagements\Core\Events\EngagementCalculationRegistryInitiated` (event id `engagement_calculation_registry_initiated`). The metric-side event is `Siren\Metrics\Core\Events\MetricCalculationRegistryInitiated` (event id `metric_calculation_registry_initiated`). Both carry the registry and a DI `InstanceProvider`, and both expose the same two methods: ```php /** * @param class-string $strategyClass */ public function addCalculationStrategy(string $strategyClass): void { $this->registry->set($strategyClass::getId(), fn() => $this->provider->get($strategyClass)); } public function deleteCalculationStrategy(string $id): void { $this->registry->delete($id); } ``` `addCalculationStrategy()` stores a lazy factory, so your strategy class is only instantiated when it is actually requested. The registry is keyed by `$strategyClass::getId()`, and registering the same id twice replaces the prior factory. Last writer wins. To remove or replace a built-in strategy, call `deleteCalculationStrategy()` with its id. Replacing a built-in under the same id is safe, because programs that stored that id still resolve to a strategy. Deleting an id that programs or distributors already use is not: their stored `calculationType` no longer resolves, so the calc credits no one and they pay out zero. Only delete an id you know is unused. ## The interface a custom calc must implement An engagement-side strategy implements `Siren\Engagements\Core\Interfaces\EngagementCalculationStrategy`, which extends `Siren\Core\Core\Interfaces\HasRequiredArgs`: ```php namespace Siren\Engagements\Core\Interfaces; use Siren\Core\Core\Interfaces\HasRequiredArgs; use Siren\Engagements\Core\Models\EngagementCalculationContext; interface EngagementCalculationStrategy extends HasRequiredArgs { /** * Stable identifier, stored in * wp_siren_program_engagement_types.calculationType and used as the * registry key. Lowercase camelCase, e.g. 'fixed', 'groupMembership'. */ public static function getId(): string; public function getName(): string; public function getDescription(): string; /** * @return EngagementCalculationResult[] */ public function calculate(EngagementCalculationContext $context): array; } ``` The members map straight onto the registry and the picker. `getId()` is the stable id stored on the row and used as the registry key. `getName()` and `getDescription()` are the label and explanation shown in the calc picker. `calculate()` produces the results to emit, one `Siren\Engagements\Core\Models\EngagementCalculationResult` per credited collaborator. The caller loops the returned array and creates one engagement record per entry. The inherited `getRequiredArgs(): string[]` lists the arg keys your strategy needs, which the save handler validates are present. `calculate()` is not handed its args directly. They are loaded from `wp_siren_configs` (config type `programEngagementTypeArg`, keyed by the context's `programEngagementTypeId`) and read inside the strategy, so the interface stays stable as per-strategy args grow. Those arg values are written when the program's engagement type is saved through the program edit endpoint. The save handler reads your `getRequiredArgs()` and rejects the save if any required arg is missing, so an operator (or a recipe's customizable fields) has to supply every one. `getRequiredArgs()` lists names only, not types, so if your strategy needs typed or validated admin inputs beyond presence, that is yours to build. The metric side is the mirror image. Implement `Siren\Metrics\Core\Interfaces\MetricCalculationStrategy` (also extends `HasRequiredArgs`) with the same members. There, `getId()` is stored in `wp_siren_distributor_metric_types.calculationType`, `calculate(MetricCalculationContext $context)` returns `Siren\Metrics\Core\Models\MetricCalculationResult[]`, and args load from config type `distributorMetricTypeArg` keyed by `distributorMetricTypeId`. The two `Result` classes take the same constructor, `(int $collaboratorId, int $score)`, so a result built on one side ports unchanged. The context objects do not match field for field, though. An `EngagementCalculationContext` exposes `program`, `triggeringCollaborator`, `opportunity`, `engagementType`, and `programEngagementTypeId`, while a `MetricCalculationContext` exposes `distributor`, `triggeringCollaborator`, `metricType`, and `distributorMetricTypeId`. There is no opportunity or program on the metric side, so any part of `calculate()` that reads those (the example below reads `opportunity` and `program`) has to be re-derived from the distributor when you port a strategy across. A strategy that needs layered walker steps (as the cascade calcs do) also implements `Siren\Core\Core\Interfaces\RequiresWalkerCapabilities` so the picker only offers it against a compatible group structure. Implementing that marker is the entire opt-in. A strategy that does not implement it declares no capability requirement, so the picker offers it for every program and distributor regardless of the bound group's structure. See [walker capabilities](/documentation/extensions/walker-capabilities) for how to write a capability-requiring calc and how the picker filters on it. This page covers registering a strategy. That page covers the capability contract a cascade strategy declares. ## Built-in calculation strategies Three calculation strategies ship today. Each is registered on both the engagement side and the metric side under the same id. `Fixed` ships in the base tier, and the two cascades ship in Pro. ### Fixed ``` ID: 'fixed' Tier: Core (Lite and up) ``` Credits a single recipient, the collaborator who triggered the engagement or metric, at a static configured value. It needs no collaborator group and ignores group structure. See [Fixed](/documentation/calculation-strategies/fixed). ### Upline cascade ``` ID: 'upline' Tier: Pro ``` Walks the bound collaborator group from the triggering collaborator toward the head of the chain or tree and credits each layer above them at its per-layer points. Requires a structure that provides the `hasLayer` capability. See [Upline cascade](/documentation/calculation-strategies/upline-cascade). ### Downline cascade ``` ID: 'downline' Tier: Pro ``` Walks the bound collaborator group from the triggering collaborator toward the leaves and credits each layer below them at its per-layer points. Also requires `hasLayer`. See [Downline cascade](/documentation/calculation-strategies/downline-cascade). ## Creating a custom strategy This engagement-side strategy credits the triggering collaborator at a configured base value, then adds a flat bonus on top once the program reaches a configured engagement-count milestone. It reads both args from `wp_siren_configs` inside `calculate()`, declares them in `getRequiredArgs()`, and emits one `EngagementCalculationResult` for the triggering collaborator. Every member maps onto the interface verified above. ```php namespace MyPlugin\Engagements; use PHPNomad\Datastore\Exceptions\DatastoreErrorException; use PHPNomad\Logger\Interfaces\LoggerStrategy; use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore; use Siren\Engagements\Core\Datastores\Engagement\Interfaces\EngagementDatastore; use Siren\Engagements\Core\Interfaces\EngagementCalculationStrategy; use Siren\Engagements\Core\Models\EngagementCalculationContext; use Siren\Engagements\Core\Models\EngagementCalculationResult; class MilestoneBonusEngagementCalculation implements EngagementCalculationStrategy { public function __construct( protected ConfigDatastore $configs, protected EngagementDatastore $engagements, protected LoggerStrategy $logger ) {} public static function getId(): string { return 'milestoneBonus'; } public function getName(): string { return 'Milestone Bonus'; } public function getDescription(): string { return 'Awards a base score, plus a flat bonus once the program passes an engagement-count milestone.'; } public function getRequiredArgs(): array { return ['baseValue', 'milestone', 'bonusValue']; } public function calculate(EngagementCalculationContext $context): array { try { $baseValue = (int) $this->configs->getConfigValue( 'programEngagementTypeArg', (string) $context->programEngagementTypeId, 'baseValue', 0 ); $milestone = (int) $this->configs->getConfigValue( 'programEngagementTypeArg', (string) $context->programEngagementTypeId, 'milestone', 0 ); $bonusValue = (int) $this->configs->getConfigValue( 'programEngagementTypeArg', (string) $context->programEngagementTypeId, 'bonusValue', 0 ); } catch (DatastoreErrorException $e) { $this->logger->logException($e); return []; } $count = $this->engagements->getActiveEngagementCount( $context->opportunity->getId(), $context->program->getId() ); $score = $count >= $milestone ? $baseValue + $bonusValue : $baseValue; return [new EngagementCalculationResult( $context->triggeringCollaborator->getId(), $score )]; } } ``` `calculate()` returns one result per credited collaborator. The trigger service loops that array and creates one engagement record per entry, so a strategy that credits a chain returns one result per layer rather than mutating any shared state. Here the strategy only ever credits the triggering collaborator, so it returns a single-element array. Returning an empty array credits no one, which is the fail-closed path. Catch your own errors inside `calculate()` and return `[]` rather than letting an exception escape, the way the example handles its datastore error and the built-in strategies do. ### Register it Write a listener that guards the event type and calls `addCalculationStrategy()` with your class: ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Engagements\Core\Events\EngagementCalculationRegistryInitiated; use MyPlugin\Engagements\MilestoneBonusEngagementCalculation; class RegisterMyEngagementCalculations implements CanHandle { public function handle(Event $event): void { if (!$event instanceof EngagementCalculationRegistryInitiated) { return; } $event->addCalculationStrategy(MilestoneBonusEngagementCalculation::class); } } ``` Then wire the listener to the event in your Initializer: ```php public function getListeners(): array { return [ EngagementCalculationRegistryInitiated::class => RegisterMyEngagementCalculations::class, ]; } ``` The metric side is identical with `MetricCalculationRegistryInitiated` and a `MetricCalculationStrategy` implementation. Swap the class names and the result type and the rest of the shape holds. ## How the first party registers the cascade strategies Pro adds the upline and downline cascade strategies through this exact seam, with no changes to the lower tiers. On the engagement side, `Siren\Pro\Core\Engagements\Listeners\RegisterProEngagementCalculations` guards `instanceof EngagementCalculationRegistryInitiated` then registers both cascade calcs: ```php public function handle(Event $event): void { if (!$event instanceof EngagementCalculationRegistryInitiated) { return; } $event->addCalculationStrategy(UplineCascadeEngagementCalculation::class); $event->addCalculationStrategy(DownlineCascadeEngagementCalculation::class); } ``` The metric side mirrors this in `Siren\Pro\Core\Metrics\Listeners\RegisterProMetricCalculations`, which guards `instanceof MetricCalculationRegistryInitiated` and registers `UplineCascadeMetricCalculation::class` and `DownlineCascadeMetricCalculation::class`. Both listeners are wired in `Siren\Pro\Core\Initializer::getListeners()`: ```php EngagementCalculationRegistryInitiated::class => RegisterProEngagementCalculations::class, MetricCalculationRegistryInitiated::class => RegisterProMetricCalculations::class, ``` The engagement and metric cascade strategies share the id `upline` (and `downline`), but they live in separate registries, so there is no collision. Each pair of cascade calcs delegates its `calculate()` to a shared cascade service and differs only in the walker-selector closure it passes (upline or downline). They also implement `RequiresWalkerCapabilities`, returning `[WalkerCapability::HAS_LAYER]`, which is what keeps them out of a flat-bound program or distributor's picker. A third-party strategy registers the same way the first party does. The seam is the same at every tier. ## How calculation strategies fit the pipeline When an engagement trigger fires, the strategy is the step that decides who gets credited and at what score: 1. A trigger fires for a program engagement type, and the trigger service builds an `EngagementCalculationContext` carrying the program, the triggering collaborator, the opportunity, the engagement type, and the `programEngagementTypeId` 2. The service reads the engagement type's `calculationType` and resolves the matching strategy from the calculation registry by that id 3. The strategy loads its own args from `wp_siren_configs` (config type `programEngagementTypeArg`, keyed by the context's `programEngagementTypeId`) inside `calculate()` 4. `calculate()` returns an `EngagementCalculationResult[]`, one entry per credited collaborator with that collaborator's id and score 5. The trigger service loops the returned array and creates one engagement record per result, all sharing the same program and opportunity 6. Those engagement records become the score data later steps read, including the resolvers and group sorters that attribute and distribute rewards downstream The metric side mirrors this exactly, swapping `MetricCalculationContext`, `distributorMetricTypeArg`, `distributorMetricTypeId`, and `MetricCalculationResult[]`. The strategy decides credit and score. It does not create the records itself, which keeps the contract small and the same at every tier. ## Custom collaborator group structures Source: https://www.sirenaffiliates.com/documentation/extensions/collaborator-group-structures Register your own structure resolver to expose a new shape of hierarchy that programs and distributors can bind to. A [collaborator group's](/documentation/general/what-are-collaborator-groups) structure decides who sits above and below whom inside the group. When a [cascade calc](/documentation/general/what-is-a-cascade) needs to pay an upline or downline, it asks the group's structure for a walker and credits each step the walker yields. The cascade engine does not know what shape the group is, whether flat, linear chain, parent-child, weighted graph, or anything else. It just consumes walker steps. That seam is what you extend here. Registering a new structure resolver introduces a new shape of hierarchy without touching the cascade engine, the picker, or any of the first-party calc strategies. Plus ships the flat resolver. Pro ships linear-chain and parent-child. Your integration can ship something else through the same event. ## The interface A structure resolver is a small object that knows its own id, knows how to describe itself in the admin UI, declares what walker capabilities it provides, and knows how to turn a group plus its members into a resolved structure. ```php namespace Siren\Plus\Core\Groups\Structure\Interfaces; use Siren\Plus\Core\Groups\Models\CollaboratorGroup; interface CollaboratorGroupStructureResolver { public static function getId(): string; public function getName(): string; public function getDescription(): string; public function resolve( CollaboratorGroup $group, array $members ): CollaboratorGroupStructure; } ``` `getId()` returns the string stored on the `CollaboratorGroup` record in the `structure` column. The first-party values are `flat`, `linearChain`, and `parentChild`. Pick something camelCase and stable. The id is a persisted reference, so once a group is saved with it, renaming or unregistering the id leaves that group unable to resolve its structure, and a cascade bound to it fails closed and credits no one. `getName()` and `getDescription()` feed the structure dropdown on the collaborator-group edit screen. The name is the option label, and the description sits underneath so operators can pick the right structure for their use case. `resolve()` is the only real work. It receives the group record and the full list of `CollaboratorGroupMember` rows that belong to it. Its job is to precompute anything the walker side will need (parent maps, position indices, adjacency lists) and hand back a `CollaboratorGroupStructure`. The resolver runs once per request the structure is needed. The returned object handles every walker call from there. Structure resolvers that participate in cascades also implement `HasProvidedWalkerCapabilities` (covered below). ## The structure interface The resolved structure is the object the cascade engine actually talks to. ```php namespace Siren\Plus\Core\Groups\Structure\Interfaces; use Novatorius\Iterator\Interfaces\CanIterate; interface CollaboratorGroupStructure { public function getUplineWalker(int $collaboratorId): CanIterate; public function getDownlineWalker(int $collaboratorId): CanIterate; public function getAllMembersWalker(): CanIterate; public function contains(int $collaboratorId): bool; } ``` Every structure exposes the same four operations. `getUplineWalker()` and `getDownlineWalker()` return walker-step value objects in cascade order, layer 1 first, then layer 2, then layer 3. `getAllMembersWalker()` yields the raw `CollaboratorGroupMember` rows for non-cascade consumers like the admin UI or a directory listing. `contains()` answers whether a given collaborator is a member of this group at all. When a direction doesn't apply, for example asking a flat group for its upline, the returned walker yields nothing. That is the honest answer, not a no-op: a flat group has no upline. Calc strategies handle empty walkers gracefully. The upline and downline walkers yield walker-step value objects (Pro's `HierarchicalWalkerStep` is the first-party example) that implement `WalkerStep` plus capability marker interfaces. The all-members walker yields raw member models. Don't mix the two. Your walker constructs each step and stamps it with its layer, the 1-indexed distance from the trigger. The cascade engine reads that layer off the step with `getLayer()`; it does not compute it for you, so a walker that yields steps in the wrong order or with the wrong layer mis-credits the cascade. The `WalkerStep` and `HasLayer` interfaces and the concrete `HierarchicalWalkerStep` all live in the Pro tier, so a structure that yields layered, cascade-compatible steps builds on Pro and can reuse `HierarchicalWalkerStep` directly, for example `new HierarchicalWalkerStep($member, $layer)`. A structure that declares no capabilities, like a flat one, yields empty directional walkers and needs none of this. ## Walker capabilities A walker step is just a small value object that wraps a `CollaboratorGroupMember` and exposes whatever the structure was able to figure out at resolve time. The pieces of data it carries are called *capabilities*, and each capability is declared by implementing a marker interface: `HasLayer`, plus whatever new ones your structure type introduces. `getProvidedWalkerCapabilities()` is the resolver's way of advertising, ahead of time, which capabilities its walker steps will carry: ```php public function getProvidedWalkerCapabilities(): array { return ['hasLayer']; } ``` The string `hasLayer` is the capability id. The first-party code references it through the `WalkerCapability::HAS_LAYER` constant, which holds that same string, so prefer the constant over a raw literal where you can reach it to avoid a silent typo. A mistyped capability id does not error, it just fails to match, and your calc quietly never appears in the picker. The picker on the program and distributor edit screens uses this list to filter the calc dropdown. A calc that requires `hasLayer` (the first-party upline and downline cascades both do) is hidden when the bound group's structure resolver doesn't declare it. That's how Siren keeps operators from picking calcs that can't possibly run against the group they bound. The first-party `hasLayer` capability says walker steps know which layer they're at, meaning distance from the trigger, 1-indexed. If your structure has a meaningful concept of weight, distance, branch position, or anything else a custom calc might want to read, declare a new capability id here and have your walker steps implement the matching marker interface. See [walker capabilities](/documentation/extensions/walker-capabilities) for the full story. A flat structure declares no capabilities. Its directional walkers are always empty, so there's nothing to advertise. ## Writing a custom resolver This is the shape of a real resolver. The first-party flat resolver is the simplest example in the codebase, with the same structure and just less to do inside `resolve()`: ```php namespace MyPlugin\Groups\Structure; use Siren\Core\Core\Interfaces\HasProvidedWalkerCapabilities; use Siren\Plus\Core\Groups\Models\CollaboratorGroup; use Siren\Plus\Core\Groups\Structure\Interfaces\CollaboratorGroupStructure; use Siren\Plus\Core\Groups\Structure\Interfaces\CollaboratorGroupStructureResolver; use Siren\Translations\Core\Services\TranslationService; final class HubAndSpokeStructureResolver implements CollaboratorGroupStructureResolver, HasProvidedWalkerCapabilities { public function __construct( protected TranslationService $translator ) {} public static function getId(): string { return 'hubAndSpoke'; } public function getName(): string { return $this->translator->translate('Hub and spoke'); } public function getDescription(): string { return $this->translator->translate( 'One hub collaborator at the center; all other members are spokes that report directly to the hub.' ); } public function resolve( CollaboratorGroup $group, array $members ): CollaboratorGroupStructure { // Precompute the hub + spoke index from metadata so the // returned structure can answer walker calls without re-scanning. return new HubAndSpokeStructure($members); } public function getProvidedWalkerCapabilities(): array { return ['hasLayer']; } } ``` The matching `HubAndSpokeStructure` class implements `CollaboratorGroupStructure`. Its upline walker for a spoke yields one step pointing at the hub at layer 1. Its downline walker for the hub yields every spoke at layer 1. For any other request the walker yields nothing. Constructor injection works the same as anywhere else in Siren: declare the dependencies you need and the container wires them. Use whatever shape makes sense for your structure. Trees, graphs, geographic regions, weighted relationships: the cascade engine doesn't care, as long as the walkers it receives yield steps in cascade order. ## Built-in structures Three structure resolvers ship today. Plus provides the flat resolver. Pro provides the linear-chain and parent-child resolvers. Each appears in the structure dropdown on the collaborator-group edit screen and is bound to a group by storing its id in the `structure` column. ### Flat ``` ID: 'flat' Tier: Plus ``` A flat collection of collaborators with no internal hierarchy. Every member is a peer, so the directional walkers always yield nothing and the resolver advertises no capabilities. See [Flat](/documentation/collaborator-group-structures/flat). ### Linear chain ``` ID: 'linearChain' Tier: Pro ``` Members are ordered by a position, where each position has at most one upline (the lower-position member) and one downline (the higher-position member). It provides the `hasLayer` capability so cascade calcs can run against it. See [Linear chain](/documentation/collaborator-group-structures/linear-chain). ### Parent-child ``` ID: 'parentChild' Tier: Pro ``` Members are arranged as a tree where each member can have one parent and any number of children. Upline walks parent to parent up to the root, and downline walks every descendant, crediting all peers at the same depth at the same per-layer rate. It provides the `hasLayer` capability. See [Parent-child](/documentation/collaborator-group-structures/parent-child). ## Registering the resolver Resolvers register through the `CollaboratorGroupStructureResolverRegistryInitiated` event. The event fires once on first read of the resolver service. Wire your listener through `getListeners()` so it runs during initialization, before that first read. A listener attached later misses the one-time broadcast and your structure never appears in the dropdown, with no error. New to Siren's event system? See the [events introduction](/documentation/developer-reference/events-introduction) and [listeners](/documentation/extensions/listeners). Listeners call `addStrategy()` with a class string, and the registry stores a lazy factory, so your resolver is only instantiated when something actually asks for it. ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Structure\Events\CollaboratorGroupStructureResolverRegistryInitiated; use MyPlugin\Groups\Structure\HubAndSpokeStructureResolver; class RegisterHubAndSpokeStructure implements CanHandle { public function handle(Event $event): void { if (!$event instanceof CollaboratorGroupStructureResolverRegistryInitiated) { return; } $event->addStrategy(HubAndSpokeStructureResolver::class); } } ``` Wire the listener in your Initializer: ```php public function getListeners(): array { return [ CollaboratorGroupStructureResolverRegistryInitiated::class => RegisterHubAndSpokeStructure::class, ]; } ``` To remove or replace a built-in resolver, call `deleteStrategy()` on the same event before adding your replacement: ```php $event->deleteStrategy('flat'); $event->addStrategy(MyFlatReplacement::class); ``` For this to be safe, your replacement must return the same `getId()` (here `flat`), because groups are already saved with that id. Deleting a built-in id that existing groups use without restoring it under the same id leaves those groups unable to resolve their structure, so their cascades fail closed. ## What appears in the admin UI Once registered, your resolver shows up automatically. The collaborator-group edit screen lists every registered resolver in the structure dropdown using `getName()` for the option label and `getDescription()` for the helper text underneath. The picker on the program and distributor edit screens reads `getProvidedWalkerCapabilities()` to filter the calc dropdown. Calcs whose required capabilities your resolver doesn't provide are hidden when a group using your structure is bound. `GET /collaborator-groups/structures` returns the full registry as JSON, including each resolver's id, name, description, and provided capabilities, so any custom admin surface or external tooling can render the same picker. That's the whole seam. Implement the two interfaces, declare your capabilities, register through the event. The cascade engine, the picker, and the admin UI pick up your structure with no further wiring. For a deeper look at how walker capabilities flow through the picker and what it takes to ship a custom calc that requires a custom capability, see [walker capabilities](/documentation/extensions/walker-capabilities). ## How structures fit the cascade pipeline During conversion processing: 1. A cascade calc fires for the triggering collaborator (an upline or downline strategy bound to a program or distributor) 2. The bound group's structure is resolved from its `structure` id into a `CollaboratorGroupStructure` 3. The calc asks the structure for an upline or downline walker, which produces walker steps in cascade order, layer 1 first 4. The calc credits each layer the walker yields at its per-layer points, one result per step The structure decides the shape of the hierarchy. The calc decides who gets credited and how much. The two stay decoupled through the walker steps the structure yields. ## Custom Conversion Types Source: https://www.sirenaffiliates.com/documentation/extensions/conversion-types Registering new conversion types that define what measurable outcomes your extension tracks. # Custom Conversion Types Conversion types represent the categories of measurable outcomes that Siren tracks. Each conversion type defines a kind of customer action (a sale, a lead capture, a subscription renewal) that can trigger rewards for collaborators. Conversion types are the bridge between what happened (the customer action) and how it gets rewarded (the incentive type). ## What defines a conversion type? ```php namespace Siren\Conversions\Core\Models; use PHPNomad\Datastore\Interfaces\DataModel; use PHPNomad\Datastore\Interfaces\HasSingleStringIdentity; class ConversionType implements DataModel, HasSingleStringIdentity { /** * @param string $id Unique identifier (e.g., 'sale', 'lead') * @param string $label Plural display label (e.g., 'Sales', 'Leads') * @param string $singularLabel Singular display label (e.g., 'Sale', 'Lead') * @param string[] $supportedIncentives Incentive type IDs that can process this conversion type */ public function __construct( string $id, string $label, string $singularLabel, array $supportedIncentives = [] ); public function getId(): string; public function getLabel(): string; public function getSingularLabel(): string; /** @return string[] */ public function getSupportedIncentives(): array; /** * Check if this conversion type supports a specific incentive type. */ public function supportsIncentive(string $incentiveTypeId): bool; } ``` ### The supportedIncentives Array The `supportedIncentives` array is the key mechanism that links conversion types to incentive types. It contains the string IDs of incentive types that know how to calculate rewards for this kind of conversion. For example, a "sale" conversion supports `saleFixedPerProduct`, `saleFixedPerTransaction`, and `saleTransactionPercentage` incentive types. A "lead" conversion only supports `leadFixed`. This prevents invalid configurations. You cannot assign a percentage-of-sale incentive to a lead conversion because leads have no transaction amount. ## Which conversion types ship out of the box? The `sale` conversion type is always registered and is the fundamental type for commerce. It supports three incentive types: `saleFixedPerProduct`, `saleFixedPerTransaction`, and `saleTransactionPercentage`. The Essentials tier adds two more. The `lead` conversion type represents non-monetary actions like form submissions or account signups, and only supports the `leadFixed` incentive type. The `renewal` conversion type represents subscription renewal payments and supports the same three sale-based incentive types as `sale`. Renewal is only registered when the active commerce extension supports the `Renewals` feature. ## Registration Pattern Conversion types are registered by listening for `ConversionTypeRegistryInitiated` and calling `addConversionType()`: ```php namespace Siren\Conversions\Core\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Conversions\Core\Events\ConversionTypeRegistryInitiated; use Siren\Conversions\Core\Models\ConversionType; class RegisterCoreConversionTypes implements CanHandle { public function handle(Event $event): void { $event->addConversionType('sale', fn() => new ConversionType( 'sale', 'Sales', 'Sale', [ 'saleFixedPerProduct', 'saleFixedPerTransaction', 'saleTransactionPercentage' ] )); } } ``` The `addConversionType()` method takes a string key and a callable factory: ```php public function addConversionType(string $field, callable $resolver): void { $this->registry->set($field, $resolver); } ``` Wire your listener to the event in your Initializer: ```php public function getListeners(): array { return [ ConversionTypeRegistryInitiated::class => RegisterCoreConversionTypes::class, ]; } ``` ### Conditional Registration Conversion types that depend on platform features can be registered conditionally using the `ExtensionRegistryService`: ```php protected function addConversionTypeIfSupported( ConversionTypeRegistryInitiated $event, string $id, string $label, string $singularLabel, array $supportedIncentives, string $feature, string ...$features ): void { if ($this->extensionRegistryService->extensionsSupportFeatures($feature, ...$features)) { $event->addConversionType($id, fn() => new ConversionType( $id, $label, $singularLabel, $supportedIncentives )); } } ``` This pattern ensures that conversion types only appear in the UI when the required commerce integration is active. ## How do I look up conversion types at runtime? The `ConversionTypeProvider` service provides access to registered conversion types at runtime: ```php namespace Siren\Conversions\Service\Datastores; class ConversionTypeProvider { /** Get a single conversion type by ID. */ public function getConversionTypeFromId(string $id): ?ConversionType; /** Get all registered conversion type IDs. */ public function getConversionTypeIdentifiers(): array; /** Get all registered ConversionType instances. */ public function getConversionTypes(): array; /** Get conversion types that support a specific incentive type. */ public function getConversionTypesForIncentive(string $incentiveTypeId): array; } ``` The `getConversionTypesForIncentive()` method is particularly useful for building UI that shows only the valid conversion types for a chosen incentive type. ## Creating a Custom Conversion Type ### When to Create One Create a custom conversion type when you have a measurable customer outcome that does not fit into sales, leads, or renewals. Examples: A trial signup (a user starts a free trial that isn't yet a sale), a course enrollment, a booked appointment, or an app install are all examples of outcomes that don't fit the built-in sale, lead, or renewal types. ### Example: Trial Signup ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Conversions\Core\Events\ConversionTypeRegistryInitiated; use Siren\Conversions\Core\Models\ConversionType; class RegisterTrialConversionType implements CanHandle { public function handle(Event $event): void { if (!$event instanceof ConversionTypeRegistryInitiated) { return; } $event->addConversionType('trial', fn() => new ConversionType( 'trial', 'Trial Signups', 'Trial Signup', ['trialFixed'] // Your custom incentive type ID )); } } ``` ### Pairing With a Custom Incentive Type A custom conversion type usually requires a corresponding custom incentive type. The `supportedIncentives` array must contain the ID of at least one incentive type that knows how to create conversions and obligations for this kind of event. The full integration requires four pieces. You need a conversion type registered via `ConversionTypeRegistryInitiated` to define the outcome category, and a corresponding incentive type registered via `IncentiveRegistryInitiated` to define how to calculate the reward. You also need a triggering event and listener — the platform event that fires when the customer action occurs (like a `TrialStarted` event) — and a conversion initializer listener that fires `ConversionInitialized` when that triggering event occurs. ### Removing or Replacing a Built-in Type Use `deleteConversionType()` on the registry event: ```php public function handle(Event $event): void { if ($event instanceof ConversionTypeRegistryInitiated) { // Remove the default sale type $event->deleteConversionType('sale'); // Add a customized version $event->addConversionType('sale', fn() => new ConversionType( 'sale', 'Purchases', // Different label 'Purchase', ['saleTransactionPercentage'] // Fewer supported incentives )); } } ``` ## How Conversion Types Fit the Pipeline ``` Customer Action -> Event -> Conversion Initializer -> ConversionInitialized -> BuildConversions (checks ConversionType + Incentive compatibility) -> Conversion Created -> Obligation -> Payout ``` When `BuildConversions` processes a `ConversionInitialized` event, it checks whether the program's incentive type is listed in the conversion type's `supportedIncentives`. If the incentive type is not supported, no conversion is created for that program. This is the enforcement mechanism that prevents misconfigured programs from generating invalid conversions. ## Custom Eligibility Resolvers Source: https://www.sirenaffiliates.com/documentation/extensions/eligibility-resolvers Implementing ProgramEligibilityResolver and DistributorEligibilityResolver to control which programs and distributors a collaborator can earn from. # Custom Eligibility Resolvers Eligibility resolvers decide which [programs](/documentation/resource-reference/programs) and distributors a collaborator is allowed to earn from. Before attribution credits a collaborator for a conversion, the eligibility service asks every registered resolver which candidate programs (or distributors) that collaborator qualifies for. A resolver is a source of eligibility. Direct collaborator-to-program bindings are one source, membership in a bound [collaborator group](/documentation/general/what-are-collaborator-groups) is another, and a custom resolver can add a third. There are two parallel seams: one for programs (engagement side) and one for distributors (metric side). They mirror each other, so anything described for the program side has an identical distributor counterpart. ## How eligibility is resolved The eligibility service **unions** the results of every registered resolver. Each resolver returns only the program ids it affirms are eligible. A resolver does not have to be exhaustive or exclusive, and it does not need to know about the other resolvers. The caller dedupes the combined set, so two resolvers affirming the same program is harmless and order across resolvers is not significant. This union model is why a program can be eligible through more than one path at once. The Core direct-binding resolver and the Plus group-bound resolver both run, and a program bound directly to a collaborator stays eligible even if that collaborator is also in a group that does not include the program. Neither path masks the other. Because the service only ever unions affirmations, eligibility is additive. A resolver can grant eligibility, never restrict it. There is no deny vote: a resolver that returns an empty set for a collaborator does not remove eligibility the other resolvers granted, it simply adds nothing. So a resolver is the wrong tool for excluding someone, a suspended collaborator for example. The only way to narrow eligibility is to remove a built-in resolver entirely (see below), which is global, not per-collaborator. Affirming a program does two things, not one. It authorizes the collaborator to earn from that program, and through `resolveAllEligibleProgramIds()` it also surfaces the program in the collaborator's REST `programs` list. Do not affirm a program you do not want the collaborator to see, not just earn from. A resolver that affirms an internal or unlaunched program for everyone exposes that program's existence over the partner API. If a resolver throws `DatastoreErrorException`, the whole eligibility check fails closed. The provider does not catch per resolver, so one resolver's throw drops every resolver's result for that check, and the attribution caller treats the failure as no eligibility, crediting no one for that conversion and logging the error. So catch recoverable errors inside your resolver and return `[]` (affirm nothing) if you want the other resolvers to still grant eligibility. Throw only when you genuinely want the entire check to fail closed. ## What interface do program eligibility resolvers implement? A program eligibility resolver implements `Siren\Engagements\Core\Interfaces\ProgramEligibilityResolver`: ```php namespace Siren\Engagements\Core\Interfaces; use PHPNomad\Datastore\Exceptions\DatastoreErrorException; interface ProgramEligibilityResolver { /** * Filter variant. Returns the subset of $candidateProgramIds the * collaborator is eligible to earn from according to this resolver. * * @param int $collaboratorId * @param int[] $candidateProgramIds Program ids the caller is asking about. * @return int[] Order is not significant. Caller dedupes. * * @throws DatastoreErrorException */ public function resolveEligibleProgramIds(int $collaboratorId, array $candidateProgramIds): array; /** * Unfiltered variant. Returns every program id this resolver affirms * the collaborator is eligible for, with no candidate filter applied. * * @return int[] Order is not significant. Caller dedupes. * * @throws DatastoreErrorException */ public function resolveAllEligibleProgramIds(int $collaboratorId): array; } ``` The two methods serve different callers. `resolveEligibleProgramIds()` is the filter variant. Attribution chokepoints already know which candidate programs they care about (for example, programs that have a particular engagement type configured), so they pass that candidate set and get back the eligible subset. `resolveAllEligibleProgramIds()` is the unfiltered variant, used by reverse-direction lookups that need every program a collaborator is associated with, such as the Collaborator's `programs` REST field. Keep the two methods consistent. `resolveEligibleProgramIds($c, $candidates)` should return exactly `resolveAllEligibleProgramIds($c)` intersected with `$candidates`. If they drift, attribution (which uses the filter variant) and the collaborator's REST `programs` field (which uses the unfiltered variant) disagree, so a collaborator can be shown a program they never earn on, or earn on one they cannot see. Implement one in terms of the other where you can. The filter variant runs on every attribution, so keep it cheap and scope any query to the `candidateProgramIds` you are handed rather than fetching the collaborator's whole eligibility each time. ## What interface do distributor eligibility resolvers implement? The distributor side is `Siren\Metrics\Core\Interfaces\DistributorEligibilityResolver`. It is a mirror of the program interface with the same union semantics: ```php namespace Siren\Metrics\Core\Interfaces; use PHPNomad\Datastore\Exceptions\DatastoreErrorException; interface DistributorEligibilityResolver { /** * @param int $collaboratorId * @param int[] $candidateDistributorIds * @return int[] * * @throws DatastoreErrorException */ public function resolveEligibleDistributorIds(int $collaboratorId, array $candidateDistributorIds): array; /** * @return int[] * * @throws DatastoreErrorException */ public function resolveAllEligibleDistributorIds(int $collaboratorId): array; } ``` ## Built-in Resolvers Siren ships four eligibility resolvers, two on the program side and two on the distributor side. The provider service runs all registered resolvers and unions their results, so a collaborator is eligible whenever any resolver says so. The Plus group-bound resolvers do not replace the Core direct-binding ones, they widen eligibility alongside them. Resolvers are keyed by class-string rather than an id (see below), so each is identified by its fully qualified class. ### DirectBindingProgramEligibilityResolver ``` Class: Siren\Engagements\Core\Services\DirectBindingProgramEligibilityResolver Tier: Core (Lite and up) ``` A collaborator is eligible for a program when they have a direct row in the `collaborators_programs` junction, the binding written when you add a collaborator to a program directly. This is the historical default and the only eligibility path on Lite and Essentials. ### DirectBindingDistributorEligibilityResolver ``` Class: Siren\Metrics\Core\Services\DirectBindingDistributorEligibilityResolver Tier: Core (Lite and up) ``` The distributor counterpart. A collaborator is eligible for a distributor when they have a direct row in the `collaboratorsDistributors` junction. ### GroupBoundProgramEligibilityResolver ``` Class: Siren\Plus\Core\Groups\Services\GroupBoundProgramEligibilityResolver Tier: Plus ``` A collaborator is eligible for a program when the program is bound to a [collaborator group](/documentation/general/what-are-collaborator-groups) the collaborator belongs to. This is what lets binding a group gate who can earn. The query detail is in [How a bound group gates eligibility](#how-a-bound-group-gates-eligibility) below. ### GroupBoundDistributorEligibilityResolver ``` Class: Siren\Plus\Core\Groups\Services\GroupBoundDistributorEligibilityResolver Tier: Plus ``` The distributor counterpart, with identical mechanics against a distributor's collaborator-group binding. ## The registry events Resolvers are registered by listening for one of two registry-initiated events. Each is broadcast once, on first use of its eligibility provider service. Wire your listener through `getListeners()` so it runs during initialization, before that first use. A listener attached later misses the one-time broadcast and your resolver never runs, with no error. New to Siren's event system? See the [events introduction](/documentation/developer-reference/events-introduction) and [listeners](/documentation/extensions/listeners). Listeners register their resolver class via `addResolver()`, and the entry is stored as a lazy factory so the resolver is only instantiated when the provider actually iterates. The program-side event is `Siren\Engagements\Core\Events\ProgramEligibilityResolverRegistryInitiated`: ``` Event ID: 'program_eligibility_resolver_registry_initiated' ``` The distributor-side event is `Siren\Metrics\Core\Events\DistributorEligibilityResolverRegistryInitiated`: ``` Event ID: 'distributor_eligibility_resolver_registry_initiated' ``` Both expose the same two methods: ```php public function addResolver(string $resolverClass): void; public function deleteResolver(string $resolverClass): void; ``` ### These events key by class-string, not getId() This is the one place where the eligibility seam differs from the other registry seams in Siren. The [incentive resolver](/documentation/extensions/incentive-resolvers), [group sorter](/documentation/extensions/group-sorters), and calculation-strategy registries all key entries by the strategy's `getId()` return value. The eligibility registry keys entries by the **class-string itself**: ```php public function addResolver(string $resolverClass): void { $this->registry->set($resolverClass, fn() => $this->provider->get($resolverClass)); } ``` There is no `getId()` call here. The eligibility resolver interfaces do not declare `getId()`, `getName()`, or `getDescription()` at all, because resolvers are never selected in the UI. They run as a set. The practical consequence is that registering the same `$resolverClass` twice replaces the prior factory (last writer wins), and `deleteResolver()` takes the resolver class-string rather than an id string. Two different classes can never collide, so each tier's resolver coexists with the others. ## How a bound group gates eligibility The group-bound resolvers are how binding a collaborator group to a program controls who can earn from it. `Siren\Plus\Core\Groups\Services\GroupBoundProgramEligibilityResolver` implements `ProgramEligibilityResolver` with this rule: a collaborator is eligible for a program if the program is bound to a collaborator group the collaborator is a member of. Bindings are stored as `wp_siren_configs` rows keyed `(type='program', subtype=, configKey='collaboratorGroupId', value=)`. To resolve eligibility, the resolver first reads the collaborator's group ids from `members->getGroupsForCollaborator()`, then queries the configs for binding rows whose `value` is in those group ids: ```php $bindings = $this->configs->andWhere([ ['column' => 'type', 'operator' => '=', 'value' => 'program'], ['column' => 'configKey', 'operator' => '=', 'value' => 'collaboratorGroupId'], ['column' => 'value', 'operator' => 'IN', 'value' => $groupIds], ['column' => 'subtype', 'operator' => 'IN', 'value' => $candidateSubtypes], ]); ``` The matching `subtype` values, cast to int, are the eligible program ids. The filter variant constrains `subtype` to the candidate set so it never overfetches. The unfiltered variant runs the same query without the `subtype` constraint. If the collaborator has no memberships, or the candidate set is empty, the resolver returns an empty array. The distributor counterpart, `Siren\Plus\Core\Groups\Services\GroupBoundDistributorEligibilityResolver`, implements `DistributorEligibilityResolver` with identical mechanics against `type='distributor'`. These group-bound resolvers run **in addition to** the Core direct-binding resolvers, `Siren\Engagements\Core\Services\DirectBindingProgramEligibilityResolver` and `Siren\Metrics\Core\Services\DirectBindingDistributorEligibilityResolver`. The provider unions both, so a program can be eligible via a direct binding or via group membership, and neither path masks the other. ## Registration pattern Register a resolver by listening for the registry event and calling `addResolver()` with your class. The first-party Plus listeners follow this shape: ```php namespace Siren\Plus\Core\Groups\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Engagements\Core\Events\ProgramEligibilityResolverRegistryInitiated; use Siren\Plus\Core\Groups\Services\GroupBoundProgramEligibilityResolver; class RegisterPlusProgramEligibilityResolvers implements CanHandle { public function handle(Event $event): void { if (!$event instanceof ProgramEligibilityResolverRegistryInitiated) { return; } $event->addResolver(GroupBoundProgramEligibilityResolver::class); } } ``` Wire the listener in your Initializer: ```php public function getListeners(): array { return [ ProgramEligibilityResolverRegistryInitiated::class => RegisterPlusProgramEligibilityResolvers::class, ]; } ``` The distributor side is wired the same way against `DistributorEligibilityResolverRegistryInitiated`. ## Creating a custom resolver A custom resolver adds a new source of eligibility. Because results are unioned, your resolver only needs to affirm the program ids it knows about. It never has to reason about programs other resolvers handle. The example below makes every collaborator eligible for a single always-on program, regardless of binding. It is deliberately broad to show the shape, but in practice you affirm narrowly, since affirming both authorizes earning and surfaces the program to the collaborator over REST: ```php namespace MyPlugin\Eligibility; use Siren\Engagements\Core\Interfaces\ProgramEligibilityResolver; class HouseProgramEligibilityResolver implements ProgramEligibilityResolver { private const HOUSE_PROGRAM_ID = 42; public function resolveEligibleProgramIds(int $collaboratorId, array $candidateProgramIds): array { return in_array(self::HOUSE_PROGRAM_ID, $candidateProgramIds, true) ? [self::HOUSE_PROGRAM_ID] : []; } public function resolveAllEligibleProgramIds(int $collaboratorId): array { return [self::HOUSE_PROGRAM_ID]; } } ``` ### Register it ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Engagements\Core\Events\ProgramEligibilityResolverRegistryInitiated; use MyPlugin\Eligibility\HouseProgramEligibilityResolver; class RegisterHouseEligibilityResolver implements CanHandle { public function handle(Event $event): void { if ($event instanceof ProgramEligibilityResolverRegistryInitiated) { $event->addResolver(HouseProgramEligibilityResolver::class); } } } ``` ## Removing or replacing a resolver Use `deleteResolver()` on the registry event. Because the registry keys by class-string, you pass the resolver class you want to remove, not an id string: ```php public function handle(Event $event): void { if ($event instanceof ProgramEligibilityResolverRegistryInitiated) { $event->deleteResolver(GroupBoundProgramEligibilityResolver::class); $event->addResolver(MyReplacementResolver::class); } } ``` Deleting a built-in is global. Removing `DirectBindingProgramEligibilityResolver` drops direct-binding eligibility for every collaborator and every program at once, not for one collaborator. Because resolvers cannot deny, deleting a built-in is the only lever that narrows eligibility, and it is a blunt one, so reach for it only when you mean to retire a whole eligibility path. ## How eligibility fits the pipeline 1. A conversion is being attributed and the system needs to know which programs a collaborator can earn from. 2. The eligibility service iterates every registered `ProgramEligibilityResolver` and calls the appropriate method (filter or unfiltered). 3. Each resolver returns the program ids it affirms. The Core direct-binding resolver returns directly bound programs, the Plus group-bound resolver returns programs bound to the collaborator's groups, and any custom resolver adds its own. 4. The service unions and dedupes the combined set. That set is the collaborator's eligibility. The distributor side runs the same flow with `DistributorEligibilityResolver` implementations. ## Custom Engagement Trigger Strategies Source: https://www.sirenaffiliates.com/documentation/extensions/engagement-triggers Implementing EngagementTriggerStrategy to define new ways collaborators earn attribution. # Custom Engagement Trigger Strategies Engagement triggers are the mechanism by which Siren attributes customer activity to collaborators. When a customer interacts with a collaborator's referral link, uses a coupon code, or is manually attributed, an engagement trigger creates or updates engagement records that link that collaborator to the customer's opportunity. These engagements are the foundation of Siren's attribution system. They determine who gets credit when a conversion eventually occurs. ## The Data Flow The engagement trigger sits early in Siren's pipeline: ``` Customer Action -> Event Fired -> Engagement Trigger Strategy -> Engagement Created -> (later) [Conversion](/documentation/resource-reference/conversions) -> [Obligation](/documentation/resource-reference/obligations) -> Payout ``` When a platform event fires (e.g., a site visit, a coupon being applied), the engagement trigger system checks all registered strategies to see which ones respond to that event. Each matching strategy inspects the event, locates the relevant collaborator, and creates or updates engagement records for every active program that collaborator participates in. ## What interface do engagement triggers implement? Every engagement trigger implements `Siren\Engagements\Core\Interfaces\EngagementTriggerStrategy`: ```php namespace Siren\Engagements\Core\Interfaces; use PHPNomad\Events\Interfaces\Event; use Siren\Engagements\Core\Models\Engagement; interface EngagementTriggerStrategy { /** * @return string The name of this strategy. */ public function getName(): string; /** * @return string A description of the strategy. */ public function getDescription(): string; /** * @return class-string[] A list of events that this trigger should fire against. */ public function getTriggeringEvents(): array; /** * Attempts to create or update engagements in the context of the specified event. * * @param Event $event The event that triggered this strategy. * @return Engagement[] Array of created or updated engagement instances. */ public function maybeCreateOrUpdateEngagements(Event $event): array; /** * Returns the unique identifier for this engagement trigger strategy type. * * @return string */ public static function getId(): string; } ``` `getId()` returns a unique string identifier (like `'referredSiteVisit'` or `'boundCouponUsed'`) that gets stored in engagement records and used for registry lookups. `getName()` and `getDescription()` provide human-readable strings for the admin UI. `getTriggeringEvents()` returns the event classes this strategy responds to, which the system uses to know which strategies to invoke for a given event. The core logic lives in `maybeCreateOrUpdateEngagements()` — it receives the event, validates it, locates the collaborator, and creates or updates engagement records, returning an array of `Engagement` models (or an empty array if the strategy doesn't apply). ## Built-in Strategies Siren ships with three engagement trigger strategies: ### ReferredSiteVisit Fires when a customer visits the site through a collaborator's referral link. It listens for `OpportunityTriggered` events with trigger type `'site_visit'` and locates the collaborator from the URL or request parameters. Strategy ID: `'referredSiteVisit'`. ### BoundCouponUsed Fires when a customer applies a coupon code that is bound to a collaborator. It listens for `CouponApplied` events and locates the collaborator by looking up the coupon code as an alias. This strategy is only registered when the active commerce integration supports coupons. Strategy ID: `'boundCouponUsed'`. ### Manual Fires when a manager manually attributes a transaction to a collaborator through the admin UI. It listens for `ManualAttributionRequested` events. Strategy ID: `'manual'`. See [Manually Attribute a Transaction](/documentation/getting-started/manually-attribute-a-transaction) for a user-level overview of when and how to use manual attribution. ## Registration Pattern Engagement triggers are registered by listening for the `EngagementTriggerRegistryInitiated` event and calling `addStrategy()` on the event object. The event uses the DI container to lazily instantiate strategies, so dependencies are auto-wired. ### How Core Strategies Are Registered In `Siren\Engagements\Service\Initializer`, the listener binding is declared: ```php public function getListeners(): array { return [ EngagementTriggerRegistryInitiated::class => RegisterCoreEngagementTriggerStrategies::class, // ... ]; } ``` The handler registers each strategy: ```php namespace Siren\Engagements\Core\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Engagements\Core\Events\EngagementTriggerRegistryInitiated; class RegisterCoreEngagementTriggerStrategies implements CanHandle { public function handle(Event $event): void { if ($event instanceof EngagementTriggerRegistryInitiated) { $event->addStrategy(ReferredSiteVisit::class); $event->addStrategy(Manual::class); // Conditionally add strategies based on feature support: // $event->addStrategy(BoundCouponUsed::class); } } } ``` The `addStrategy()` method on the event registers a lazy factory in the registry: ```php public function addStrategy(string $strategyClass): void { $this->registry->set($strategyClass::getId(), fn() => $this->provider->get($strategyClass)); } ``` ## Creating a Custom Engagement Trigger Here is a complete example of a custom trigger that creates engagements when a student completes a course lesson: ### Step 1: Define Your Event If your triggering event does not already exist in Siren, create one: ```php namespace MyPlugin\Events; use PHPNomad\Events\Interfaces\Event; use Siren\Opportunities\Core\Models\Opportunity; class LessonCompleted implements Event { public function __construct( protected int $studentId, protected int $lessonId, protected Opportunity $opportunity ) {} public function getStudentId(): int { return $this->studentId; } public function getLessonId(): int { return $this->lessonId; } public function getOpportunity(): Opportunity { return $this->opportunity; } public static function getId(): string { return 'lesson_completed'; } } ``` ### Step 2: Implement the Strategy ```php namespace MyPlugin\Engagements; use PHPNomad\Events\Interfaces\Event; use PHPNomad\Utils\Helpers\Arr; use Siren\Engagements\Core\Interfaces\EngagementTriggerStrategy; use Siren\Engagements\Core\Services\CollaboratorActiveProgramService; use Siren\Engagements\Core\Services\EngagementTriggerService; use Siren\Programs\Core\Models\Program; use MyPlugin\Events\LessonCompleted; use MyPlugin\Services\CollaboratorFromStudent; class LessonCompletionTrigger implements EngagementTriggerStrategy { public function __construct( protected CollaboratorActiveProgramService $activeProgramService, protected EngagementTriggerService $engagementTriggerService, protected CollaboratorFromStudent $collaboratorFromStudent ) {} public function getName(): string { return 'Lesson Completed'; } public function getDescription(): string { return 'Triggers an engagement when a referred student completes a lesson.'; } public function getTriggeringEvents(): array { return [LessonCompleted::class]; } public function maybeCreateOrUpdateEngagements(Event $event): array { if (!$event instanceof LessonCompleted) { return []; } // Locate the collaborator who referred this student $collaborator = $this->collaboratorFromStudent->locate($event->getStudentId()); if (!$collaborator) { return []; } // Get all active programs this collaborator participates in // that support this engagement type $programs = $this->activeProgramService->getActiveProgramIds( $this->getId(), $collaborator->getId() ); return Arr::map( $programs, fn(Program $program) => $this->engagementTriggerService->triggerEngagement( $this->getId(), $program, $collaborator, $event->getOpportunity() ) ); } public static function getId(): string { return 'lessonCompleted'; } } ``` ### Step 3: Register the Strategy Create a listener and wire it in your Initializer: ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Engagements\Core\Events\EngagementTriggerRegistryInitiated; use MyPlugin\Engagements\LessonCompletionTrigger; class RegisterLessonCompletionTrigger implements CanHandle { public function handle(Event $event): void { if ($event instanceof EngagementTriggerRegistryInitiated) { $event->addStrategy(LessonCompletionTrigger::class); } } } ``` In your Initializer: ```php public function getListeners(): array { return [ EngagementTriggerRegistryInitiated::class => RegisterLessonCompletionTrigger::class, ]; } ``` ## Key Services The `EngagementTriggerService` is the workhorse that custom strategies should delegate to. Its `triggerEngagement()` method handles: - Validating that the program, collaborator, and opportunity are all active - Finding or creating the engagement record - Calculating and accumulating the engagement score based on program configuration - Returning the resulting `Engagement` model The `CollaboratorActiveProgramService` determines which programs a collaborator is enrolled in that support a given engagement type. Always use this to scope your trigger to the correct programs. ## Removing or Replacing a Built-in Strategy The registry event also supports `deleteStrategy()`: ```php public function handle(Event $event): void { if ($event instanceof EngagementTriggerRegistryInitiated) { // Remove a built-in strategy $event->deleteStrategy('referredSiteVisit'); // Replace it with your own $event->addStrategy(MyCustomSiteVisit::class); } } ``` This is useful when you need to change the behavior of a built-in trigger without creating a second, competing strategy. ## Custom Group Sorters Source: https://www.sirenaffiliates.com/documentation/extensions/group-sorters Implementing program group sorting strategies for multi-program attribution priority. # Custom Group Sorters When a business runs multiple [programs](/documentation/resource-reference/programs), a single conversion could qualify for rewards under more than one program. A standard affiliate program and a premium partner program are a common example. [Program groups](/documentation/resource-reference/program-groups) solve this by grouping related programs together and using a sorter strategy to determine which program takes priority. Group sorters are the mechanism that decides attribution order when programs compete. ## The Problem They Solve Consider an e-commerce store with two programs in the same program group: A standard affiliates program paying 10% commission and a premium partners program paying 20% commission. A customer clicks Affiliate A's link, then later clicks Partner B's link, and then makes a purchase. Both programs have active engagements for this opportunity. The program group's sorter determines which program gets to process the conversion first (and in many configurations, exclusively). ## What interface do group sorters implement? ```php namespace Siren\ProgramGroups\Core\Interfaces; interface ProgramSorterStrategy { /** * Sorts the provided programs in the order they should run. * * @param array $programs Array of Program models in the group. * @param int $opportunityId The opportunity being converted. * @return array Programs sorted by priority (first = highest priority). */ public function sortPrograms(array $programs, int $opportunityId): array; /** * Human-readable name for UI display and selection. */ public function getName(): string; /** * Brief description of the sorting behavior. */ public function getDescription(): string; /** * Unique identifier in camelCase format. */ public static function getId(): string; } ``` ### How does program sorting work? The method receives all programs in the group and the opportunity ID. It must return the same programs in priority order. The first program in the returned array gets first claim on the conversion. Programs that should not receive credit can be excluded from the returned array entirely. The opportunity ID is essential because the sort decision typically depends on engagement data. The sorter needs to know which collaborator engaged first, most recently, or most frequently for that specific opportunity. ## Built-in Sorters ### OldestBindingWins ``` ID: 'oldestBindingWins' ``` Sorts programs by the `dateModified` of their engagements in ascending order. The program whose engagement was created earliest takes priority. This rewards the first collaborator to begin tracking the customer. Implementation detail: Queries the engagement datastore for engagements matching the opportunity and the programs in the group, sorted by `dateModified` ASC: ```php $engagements = $this->engagements->andWhere([ ['column' => 'programId', 'operator' => 'IN', 'value' => Arr::pluck($programs, 'id')], ['column' => 'opportunityId', 'operator' => '=', 'value' => $opportunityId] ], null, null, 'dateModified'); ``` ### NewestBindingWins ``` ID: 'newestBindingWins' ``` Sorts programs by the `dateModified` of their engagements in descending order. The program whose engagement was most recently created or updated takes priority. This rewards the most recent collaborator interaction. It is the "last touch" attribution model. Implementation detail: Same query as `OldestBindingWins` but sorted `DESC`: ```php $engagements = $this->engagements->andWhere([ ['column' => 'programId', 'operator' => 'IN', 'value' => Arr::pluck($programs, 'id')], ['column' => 'opportunityId', 'operator' => '=', 'value' => $opportunityId] ], null, null, 'dateModified', 'DESC'); ``` ## Registration Pattern Group sorters are registered by listening for `GroupSorterStorageRegistryInitiated` and calling `addSorter()`: ```php namespace Siren\Collaborators\Service\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Collaborators\Core\Events\GroupSorterStorageRegistryInitiated; use Siren\ProgramGroups\Core\GroupSorters\NewestBindingWins; use Siren\ProgramGroups\Core\GroupSorters\OldestBindingWins; class SetupCoreGroupSorters implements CanHandle { public function handle(Event $event): void { if (!$event instanceof GroupSorterStorageRegistryInitiated) { return; } $event->addSorter(NewestBindingWins::class); $event->addSorter(OldestBindingWins::class); } } ``` The `addSorter()` method registers a lazy factory: ```php public function addSorter(string $sorterClass) { $this->registry->set( $sorterClass::getId(), fn() => $this->provider->get($sorterClass) ); } ``` Wire the listener in your Initializer: ```php public function getListeners(): array { return [ GroupSorterStorageRegistryInitiated::class => SetupCoreGroupSorters::class, ]; } ``` ## How do I access sorters at runtime? The `GroupSorterStorageService` provides runtime access to sorters: ```php namespace Siren\Collaborators\Core\Services; class GroupSorterStorageService { /** Get a sorter by ID. */ public function get(string $id): ?ProgramSorterStrategy; /** Get the sorter assigned to a specific program group. */ public function getSorterForGroup(int $groupId): ?ProgramSorterStrategy; /** Get all registered sorters. */ public function getSorters(): array; } ``` The `getSorterForGroup()` method looks up the program group record, reads its `sorter` field (which stores the sorter ID string), and resolves it from the registry. ## Creating a Custom Group Sorter ### When to Create One Create a custom sorter when the built-in "oldest" and "newest" engagement models do not match your attribution requirements. You might want the program with the highest engagement score to win, or you might want higher-tier programs to always beat lower-tier ones, or you might want to sort by historical revenue contribution, or even randomly select a program for A/B testing. ### Example: Highest Score Wins This sorter gives priority to the program whose engagement has the highest score. That means the collaborator who generated the most engagement activity for that opportunity wins: ```php namespace MyPlugin\GroupSorters; use PHPNomad\Utils\Helpers\Arr; use Siren\Engagements\Core\Datastores\Engagement\Interfaces\EngagementDatastore; use Siren\Engagements\Core\Models\Engagement; use Siren\ProgramGroups\Core\Interfaces\ProgramSorterStrategy; use Siren\Programs\Core\Models\Program; class HighestScoreWins implements ProgramSorterStrategy { public function __construct( protected EngagementDatastore $engagements ) {} public function sortPrograms(array $programs, int $opportunityId): array { // Fetch engagements for all programs in the group /** @var Engagement[] $engagements */ $engagements = $this->engagements->andWhere([ ['column' => 'programId', 'operator' => 'IN', 'value' => Arr::pluck($programs, 'id')], ['column' => 'opportunityId', 'operator' => '=', 'value' => $opportunityId] ]); // Sort engagements by score descending usort($engagements, fn(Engagement $a, Engagement $b) => $b->getScore() <=> $a->getScore()); // Map back to programs in score order $result = []; foreach ($engagements as $engagement) { $program = Arr::find( $programs, fn(Program $p) => $p->getId() === $engagement->getProgramId() ); if ($program) { $result[] = $program; } } return Arr::whereNotNull($result); } public static function getId(): string { return 'highestScoreWins'; } public function getName(): string { return 'Highest Engagement Score Wins'; } public function getDescription(): string { return 'Selects the program whose collaborator has the highest engagement score.'; } } ``` ### Register It ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Collaborators\Core\Events\GroupSorterStorageRegistryInitiated; use MyPlugin\GroupSorters\HighestScoreWins; class RegisterHighestScoreSorter implements CanHandle { public function handle(Event $event): void { if ($event instanceof GroupSorterStorageRegistryInitiated) { $event->addSorter(HighestScoreWins::class); } } } ``` Wire it in your Initializer: ```php public function getListeners(): array { return [ GroupSorterStorageRegistryInitiated::class => RegisterHighestScoreSorter::class, ]; } ``` ## Removing or Replacing a Built-in Sorter Use `deleteSorter()` on the registry event: ```php public function handle(Event $event): void { if ($event instanceof GroupSorterStorageRegistryInitiated) { $event->deleteSorter('oldestBindingWins'); $event->addSorter(MyCustomSorter::class); } } ``` ## How Group Sorters Fit the Pipeline During conversion processing: 1. A conversion event fires (e.g., `ConversionInitialized`) 2. `BuildConversions` identifies all programs with active engagements for this opportunity 3. For programs in a **program group**, the group's sorter is resolved via `GroupSorterStorageService::getSorterForGroup()` 4. The sorter's `sortPrograms()` orders the competing programs by priority 5. Typically only the first program in the sorted order processes the conversion. The others are skipped. 6. This ensures a single conversion does not generate duplicate rewards across competing programs The sorter does not decide whether a program wins or loses. It decides the *order* in which programs are evaluated. The conversion system uses that order to give priority, usually processing only the first program with a valid engagement. ## Custom Incentive Types & Resolvers Source: https://www.sirenaffiliates.com/documentation/extensions/incentive-resolvers Implementing Incentive and IncentiveResolver interfaces for custom reward calculations. # Custom Incentive Types & Resolvers Siren separates the concept of *what* triggers a reward (the Incentive) from *how* the reward is distributed among competing collaborators (the IncentiveResolver). This separation allows flexible commission structures. The same incentive type (e.g., percentage-of-sale) can use different attribution models (e.g., oldest engagement wins, evenly shared pool). ## The Incentive Interface The `Incentive` interface defines a reward structure. It specifies what conversions it applies to and how it creates conversions and obligations: ```php namespace Siren\Incentives\Core\Interfaces; use Siren\Programs\Core\Models\Program; use Siren\Conversions\Core\Models\Conversion; use Siren\Obligations\Core\Models\Obligation; use Siren\Transactions\Core\Models\Transaction; interface Incentive { /** * Whether this incentive should run for the given program. */ public function shouldRun(Program $program): bool; /** * The conversion types this incentive supports (e.g., ['sale', 'renewal']). */ public function getConversionTypes(): array; /** * Creates Conversion records from this incentive structure. * * @param int $opportunityId * @param int $programId * @param Transaction|null $transaction * @param string $conversionType * @return Conversion[] */ public function maybeCreateConversions( int $opportunityId, int $programId, ?Transaction $transaction, string $conversionType ): array; /** * Re-creates conversions for renewals using existing engagements. */ public function renewConversions( array $engagements, ?Transaction $transaction, string $conversionType ): array; /** * Creates an obligation from a conversion, using the resolver to calculate amount. */ public function maybeCreateObligation( Conversion $conversion, ?Transaction $transaction, IncentiveResolver $incentiveResolver ): ?Obligation; /** * Creates a renewal obligation using provided engagements. */ public function maybeRenewObligation( Conversion $conversion, ?Transaction $transaction, IncentiveResolver $incentiveResolver, array $engagements ): ?Obligation; public static function getId(): string; public static function getName(): string; public static function getDescription(): string; } ``` Incentive types define the reward *calculation* (e.g., "10% of the transaction total" or "$5 per product"). They are responsible for creating both the conversion records and the resulting obligations. ## What interface do incentive resolvers implement? The `IncentiveResolver` determines how the calculated reward amount is distributed when multiple collaborators have engaged with the same opportunity. It answers: "Given a reward pool, which collaborator(s) get how much?" ```php namespace Siren\Incentives\Core\Interfaces; use Siren\Commerce\Models\Amount; use Siren\Conversions\Core\Models\Conversion; use Siren\Transactions\Core\Models\Transaction; interface IncentiveResolver { /** * Resolves the actual reward amount for a specific conversion. * * @param Amount $rewardPool The total reward available. * @param Conversion $conversion The conversion being resolved. * @param Transaction|null $transaction The associated transaction. * @return Amount The resolved reward amount for this conversion's collaborator. */ public function getRewardAmount( Amount $rewardPool, Conversion $conversion, ?Transaction $transaction ): Amount; public static function getId(): string; public static function getName(): string; public static function getDescription(): string; } ``` ### The Amount Model The `Amount` model represents a monetary value in the smallest currency unit (e.g., cents for USD): ```php namespace Siren\Commerce\Models; class Amount { public function __construct(int $value, Currency $currency) {} public function getValue(): int {} public function getCurrency(): Currency {} } ``` When a resolver returns `new Amount(0, $rewardPool->getCurrency())`, that collaborator receives nothing. When it returns the full `$rewardPool`, that collaborator receives the entire reward. ## Built-in Resolvers ### Core Resolvers (Lite) The core resolvers handle the most common attribution models. `OldestBindingWins` gives the full reward to whichever collaborator engaged first, while `NewestBindingWins` does the opposite. The most recent engagement takes all. Both support renewals via `CanRenew`. ### Essentials Resolvers The Essentials tier adds more nuanced options. `EvenlySharedPool` divides the reward equally among all collaborators with active engagements, using `Num::getDividedInt()` for integer division in the smallest currency unit. `TopScoreWins` gives the full reward to the collaborator with the highest engagement score, where scores accumulate based on program configuration (e.g., each site visit adds points). `EveryBindingWins` pays the full reward to every collaborator, which means the total payout can exceed the original reward pool. And `PerformanceSharePool` distributes proportionally based on relative engagement scores. Higher scores get a larger share. ## Registration Pattern Resolvers are registered by listening for the `IncentiveResolverRegistryInitiated` event: ```php namespace Siren\Incentives\Core\Handlers; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Incentives\Core\Events\IncentiveResolverRegistryInitiated; class RegisterCoreIncentiveResolvers implements CanHandle { public function handle(Event $event): void { $event->addStrategy(NewestBindingWins::class); $event->addStrategy(OldestBindingWins::class); } } ``` The `addStrategy()` method registers a lazy factory using the DI container: ```php public function addStrategy(string $strategyClass): void { $this->registry->set( $strategyClass::getId(), fn() => $this->provider->get($strategyClass) ); } ``` Wire your listener in your Initializer: ```php public function getListeners(): array { return [ IncentiveResolverRegistryInitiated::class => RegisterMyResolvers::class, ]; } ``` ## Creating a Custom Resolver ### Example: Tiered Commission Resolver This resolver gives higher-performing collaborators a larger share of the reward pool, using configurable tiers based on total engagement count: ```php namespace MyPlugin\Incentives; use Siren\Commerce\Models\Amount; use Siren\Conversions\Core\Models\Conversion; use Siren\Engagements\Core\Datastores\Engagement\Interfaces\EngagementDatastore; use Siren\Engagements\Core\Models\Engagement; use Siren\Incentives\Core\Interfaces\IncentiveResolver; use Siren\Transactions\Core\Models\Transaction; class TieredCommissionResolver implements IncentiveResolver { public function __construct( protected EngagementDatastore $engagements ) {} public function getRewardAmount( Amount $rewardPool, Conversion $conversion, ?Transaction $transaction ): Amount { /** @var Engagement $engagement */ $engagement = $this->engagements->find($conversion->getEngagementId()); // Count total historical engagements for this collaborator $totalEngagements = $this->engagements->getActiveEngagementCount( $engagement->getOpportunityId(), $engagement->getProgramId() ); // Apply tier multiplier based on engagement volume $multiplier = match (true) { $totalEngagements >= 100 => 1.5, // 150% for high performers $totalEngagements >= 50 => 1.25, // 125% for mid-tier $totalEngagements >= 10 => 1.0, // 100% baseline default => 0.75, // 75% for newcomers }; $amount = (int) round($rewardPool->getValue() * $multiplier); return new Amount($amount, $rewardPool->getCurrency()); } public static function getId(): string { return 'tieredCommission'; } public static function getName(): string { return 'Tiered Commission'; } public static function getDescription(): string { return 'Adjusts commission based on collaborator engagement volume.'; } } ``` ### Register It ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Incentives\Core\Events\IncentiveResolverRegistryInitiated; use MyPlugin\Incentives\TieredCommissionResolver; class RegisterTieredResolver implements CanHandle { public function handle(Event $event): void { if ($event instanceof IncentiveResolverRegistryInitiated) { $event->addStrategy(TieredCommissionResolver::class); } } } ``` ## Supporting Renewals If your resolver needs to handle subscription renewals, implement the `CanRenew` interface: ```php use Siren\Incentives\Core\Interfaces\CanRenew; class MyResolver implements IncentiveResolver, CanRenew { public function getRewardAmount(Amount $rewardPool, Conversion $conversion, ?Transaction $transaction): Amount { // Initial conversion logic } public function getRenewalRewardAmount( Amount $rewardPool, Conversion $conversion, ?Transaction $transaction, array $engagements ): Amount { // Renewal logic -- engagements are passed directly // since the original opportunity may no longer be active } } ``` The renewal method receives the engagements array directly rather than looking them up by opportunity, because during renewals the original opportunity context may have changed. ## Removing or Replacing a Built-in Resolver Use `deleteStrategy()` on the registry event: ```php public function handle(Event $event): void { if ($event instanceof IncentiveResolverRegistryInitiated) { $event->deleteStrategy('oldestBindingWins'); $event->addStrategy(MyCustomResolver::class); } } ``` ## How It All Connects 1. An **Incentive** (e.g., `SaleTransactionPercentage`) calculates the reward pool from the transaction amount 2. The Incentive calls `maybeCreateObligation()`, passing the reward pool to the **IncentiveResolver** 3. The **IncentiveResolver** (e.g., `OldestBindingWins`) determines how much of that pool this specific collaborator receives 4. The resulting `Amount` becomes the obligation value. This is what the business owes that collaborator. ## Custom Transaction Compilers Source: https://www.sirenaffiliates.com/documentation/extensions/transaction-compilers Building custom transaction compilation strategies that control obligation calculations. # Custom Transaction Compilers Transaction compilers (formally "transaction detail filter strategies") control which parts of a transaction are included in commission calculations. When a sale occurs, the transaction contains multiple detail lines. These include product line items, taxes, shipping charges, discounts, and fees. Transaction compilers determine which of these detail types flow into the reward calculation for each program or distribution. For a user-level overview of how these filters are configured through the admin UI, see [Transaction Filtering](/documentation/general/line-item-filters). ## The Problem They Solve Consider a sale with these transaction details: | Detail Type | Amount | |-------------|--------| | Product A (line item) | $50.00 | | Product B (line item) | $30.00 | | Shipping | $8.00 | | Tax | $6.40 | | Discount | -$10.00 | Should the collaborator earn commission on the $80.00 product total? On the $84.40 after-tax total? On the $74.40 after discount? Transaction compilers answer this question by filtering which `TransactionDetail` records are included in the reward calculation. ## What interface do transaction compilers implement? ```php namespace Siren\Incentives\Core\Interfaces; use Siren\Conversions\Core\Models\Conversion; use Siren\Transactions\Core\Models\TransactionDetail; interface TransactionDetailFilterStrategy { /** * Returns true if the provided transaction detail should be included * for the given program and conversion. */ public function shouldIncludeTransactionDetailForProgram( TransactionDetail $detail, int $programId, Conversion $conversion ): bool; /** * Returns true if the provided transaction detail should be included * for the given distribution. */ public function shouldIncludeTransactionDetailForDistributor( TransactionDetail $detail, int $distributionId ): bool; /** * The unique identifier for this filter strategy. */ public static function getId(): string; /** * Human-readable name for the admin UI. */ public function getName(): string; /** * Brief description of what this filter includes. */ public function getDescription(): string; } ``` Each filter strategy answers two questions via separate methods. One is for program-based rewards (instant commissions) and the other is for distribution-based rewards (scheduled payouts). The program method receives additional context via the `Conversion` object. ## The Compiler Models Transaction compilers are linked to programs and distributions through junction models: ### ProgramTransactionCompiler ```php namespace Siren\Programs\Core\Models; class ProgramTransactionCompiler implements DataModel, HasSingleIntIdentity { public function getProgramId(): int; public function getTransactionCompiler(): string; // The filter strategy ID } ``` ### DistributionTransactionCompiler ```php namespace Siren\Distributions\Core\Models; class DistributionTransactionCompiler implements DataModel, HasSingleIntIdentity { public function getDistributionId(): int; public function getTransactionCompiler(): string; // The filter strategy ID } ``` A program or distribution can have multiple compilers bound to it. Each compiler references a filter strategy by its string ID (e.g., `'includeLineItems'`, `'includeTaxes'`). ## How does the compilation process work? The `TransactionCompilerService` orchestrates the filtering. It initializes the registry on first access, then applies compilers to transaction details: ```php namespace Siren\Distributions\Core\Services; class TransactionCompilerService { /** * Filters transaction details for a program. * * @param TransactionDetail[] $inputTransactionDetails * @param ProgramTransactionCompiler[]|DistributionTransactionCompiler[] $compilers * @param int $programId * @param Conversion $conversion * @return TransactionDetail[] */ public function compileTransactionDetailsForProgram( array $inputTransactionDetails, array $compilers, int $programId, Conversion $conversion ): array; /** * Filters transaction details for a distribution. */ public function compileTransactionDetailsForDistribution( array $inputTransactionDetails, array $compilers, int $distributionId ): array; } ``` The compilation process iterates through each bound compiler model, resolves the corresponding filter strategy from the registry, and tests each transaction detail against it. Details that pass any compiler's filter are included; details consumed by one compiler are removed from the pool so they are not double-counted. ## What filters ship out of the box? Siren ships with five built-in filters, one for each transaction detail type. `includeLineItems` passes product line items through to the calculation and is the most complex of the five. It can further filter by product categories, SKUs, line item type, and whether the product must be "owned" by the collaborator. `includeTaxes` passes tax charges, `includeShipping` passes shipping charges, `includeDiscounts` passes discount line items, and `includeFees` passes fee line items. Each filter is a simple type check against the transaction detail's `getType()` value. ## Registration Pattern Filter strategies are registered by listening for `TransactionFilterRegistryInitiated`: ```php namespace Siren\Incentives\Core\Handlers; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Incentives\Core\Events\TransactionFilterRegistryInitiated; class RegisterCoreTransactionDetailFilters implements CanHandle { public function handle(Event $event): void { $event->addStrategy(IncludeDiscounts::class); $event->addStrategy(IncludeFees::class); $event->addStrategy(IncludeLineItems::class); $event->addStrategy(IncludeShipping::class); $event->addStrategy(IncludeTaxes::class); } } ``` The event provides `addStrategy()` and `deleteStrategy()`: ```php public function addStrategy(string $strategyClass): void { $this->registry->set( $strategyClass::getId(), fn() => $this->provider->get($strategyClass) ); } ``` ## Creating a Custom Transaction Compiler ### Example: Include Only Digital Products This filter includes transaction details only for digital/downloadable products, identified by a `'digital'` attribute: ```php namespace MyPlugin\TransactionFilters; use Siren\Conversions\Core\Models\Conversion; use Siren\Incentives\Core\Interfaces\TransactionDetailFilterStrategy; use Siren\Transactions\Core\Datastores\TransactionDetailAttribute\Interfaces\TransactionDetailAttributeDatastore; use Siren\Transactions\Core\Models\TransactionDetail; class IncludeDigitalProducts implements TransactionDetailFilterStrategy { public function __construct( protected TransactionDetailAttributeDatastore $attributes ) {} public function shouldIncludeTransactionDetailForProgram( TransactionDetail $detail, int $programId, Conversion $conversion ): bool { return $this->isDigital($detail); } public function shouldIncludeTransactionDetailForDistributor( TransactionDetail $detail, int $distributionId ): bool { return $this->isDigital($detail); } private function isDigital(TransactionDetail $detail): bool { if ($detail->getType() !== 'line_item') { return false; } try { $isDigital = $this->attributes->getAttributeValue( $detail->getId(), 'digital', 'false' ); return $isDigital === 'true'; } catch (\Exception $e) { return false; } } public static function getId(): string { return 'includeDigitalProducts'; } public function getName(): string { return 'Digital Products Only'; } public function getDescription(): string { return 'Include only digital/downloadable product line items in commission calculations.'; } } ``` ### Register It ```php namespace MyPlugin\Listeners; use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Incentives\Core\Events\TransactionFilterRegistryInitiated; use MyPlugin\TransactionFilters\IncludeDigitalProducts; class RegisterDigitalProductFilter implements CanHandle { public function handle(Event $event): void { if ($event instanceof TransactionFilterRegistryInitiated) { $event->addStrategy(IncludeDigitalProducts::class); } } } ``` Wire the listener in your Initializer: ```php public function getListeners(): array { return [ TransactionFilterRegistryInitiated::class => RegisterDigitalProductFilter::class, ]; } ``` ## How Compilers Are Applied When a conversion is processed: 1. The system loads the `ProgramTransactionCompiler` records bound to the program 2. The `TransactionCompilerService` iterates through each compiler 3. For each compiler, it resolves the `TransactionDetailFilterStrategy` from the registry 4. Each transaction detail is tested against the filter's `shouldIncludeTransactionDetailForProgram()` method 5. Details that pass are added to the accumulator and removed from the input pool 6. The accumulated details are used to calculate the reward amount This means a program configured with `includeLineItems` and `includeTaxes` will base commissions on product prices plus tax, but exclude shipping and discounts. The order of compiler evaluation does not affect the result. Each detail can only be included once. ## Customer Journey Examples Source: https://www.sirenaffiliates.com/documentation/getting-started/customer-journey-examples End-to-end walkthroughs of how Siren handles common attribution scenarios: simple sales, multi-program payouts, and platform-specific cases for WooCommerce and LifterLMS. This page walks through a handful of real customer journeys so you can see how Siren's engagement, conversion, and obligation pipeline actually runs end-to-end. It's aimed at anyone who's finished setting up their first program and wants to confirm the pieces fit together the way they expect. Each scenario uses concrete numbers and names so you can follow along, compare against your own admin, and catch misconfigurations before they turn into payout surprises. ## A simple affiliate sale Start with the most common scenario. Affiliate A shares their referral link, a visitor clicks it, and Siren records that click as an [engagement](/documentation/general/what-is-an-engagement) tied to Affiliate A. The association is stored in a cookie so Affiliate A keeps credit even if the customer doesn't buy right away. How long that credit lasts is determined by the expiration time you set when you [created the program](/documentation/getting-started/create-a-program-in-siren). The visitor browses, adds a beanie and three belts to the cart, and checks out for $183. When the order is placed, Siren creates a [transaction](/documentation/general/what-are-transactions) for the order details and a [conversion](/documentation/general/what-is-a-conversion) linked to Affiliate A's program. You can find the new conversion under Siren > Conversions in the WordPress admin. If your payment gateway finalizes orders automatically (like Stripe), the conversion goes straight to complete and an [obligation](/documentation/general/what-are-obligations) is created for the calculated commission. If you're using something like check payments that puts the order on hold, the conversion sits in "depending" status until the order is marked completed in your commerce plugin. Once the order is completed, the obligation appears and shows exactly what you owe Affiliate A (in this case, $10.95). Nothing else needs to happen until you're ready to [pay them out](/documentation/getting-started/how-to-pay-collaborators). ## Multiple programs paying out on one sale Siren can create more than one conversion from a single transaction, as long as the programs aren't competing inside the same [program group](/documentation/general/what-are-program-groups). This is how you stack rewards for different kinds of contributions without them interfering with each other. Picture a site with two programs running side by side. The first is an affiliate program that pays 3% on sales. The second is a blog content program that pays 1% to authors whose posts get read before a purchase. Neither program is in a group, so they evaluate independently. A customer arrives through Affiliate A's referral link, then later reads a blog post written by Author B before heading to the shop and buying $180 worth of product. At that point, Siren has two engagements on file: one for Affiliate A (the referral click) and one for Author B (the blog post view), attached to two different programs that don't share a group. When the order completes, Siren evaluates each program separately. The affiliate program fires for Affiliate A and creates a conversion worth $5.40. The blog content program fires for Author B and creates a separate conversion worth $1.80. One transaction, two conversions, two obligations, two collaborators getting paid for different contributions to the same sale. The transaction screen in the admin shows both obligations and the total payout across all programs for that order. This is what Siren does by default. As long as programs aren't grouped, every program that matches a transaction will fire. That's the opposite of how most affiliate plugins work, and it's intentional: it lets you reward different kinds of contributions (referrals, content, co-marketing, product ownership) without forcing them to compete for the same payout. ## Using a program group to prevent double-pays Now flip the scenario. Say you want a standard affiliate program at 3% and a super-affiliate program at 5% for your top performers. You don't want both to fire when a super-affiliate makes a referral, because that would pay them twice for the same sale. This is exactly what program groups are for. You bundle the two affiliate programs into a group (for example, using [newest engagement wins](/documentation/program-group-structures/newest-engagement-wins)), and Siren will pick exactly one winner per conversion. With the group in place, a super-affiliate's referral fires only the super-affiliate program. A standard affiliate's referral fires only the standard affiliate program. Even if a customer somehow ends up with engagements from both (say they clicked a standard affiliate's link on Monday and a super-affiliate's link on Wednesday), the group's structure decides who wins. Newest engagement wins picks whichever click or view happened most recently, so the super-affiliate gets the credit in that example. A blog content program that's left outside the group keeps paying independently, because grouping is only about programs that would otherwise overlap. If you're not sure which structure fits your setup, the [structure comparison page](/documentation/program-group-structures/choosing-a-program-group-structure) walks through the options. ## Course platform: instructor royalty plus external affiliate LifterLMS sites often layer a royalty program on top of their affiliate programs, which creates a useful case study for how grouped and ungrouped programs interact. Imagine three programs on a course site: - A standard affiliate program at 25%, open to anyone. - An instructor affiliate program at 47%, reserved for course creators promoting their own work. - An instructor royalty program at 50% that pays the course owner whenever their course sells, no matter who referred the buyer. The two affiliate programs are wrapped in a program group using newest engagement wins. The royalty program is deliberately left out of the group because it rewards authorship, not marketing, and should always pay regardless of who made the referral. Say Author B owns a $100 course. When Author B shares their own referral link and a student buys through it, Siren creates two conversions. The first is from the instructor affiliate program ($47), because Author B's referral engagement won inside the affiliate group. The second is from the instructor royalty program ($50), because Author B owns the course and the royalty program isn't in the group. Author B walks away with $97 of the $100 sale, and the platform keeps $3. Now swap the referrer. Affiliate A shares the same course and a different student buys. This time Affiliate A earns $25 from the standard affiliate program, and Author B still earns $50 from the royalty program because they own the course. Two collaborators, two conversions, one transaction. The royalty program fires regardless of who referred the sale, because it isn't in the affiliate group and isn't competing with anything. This layering is how course platforms reward instructors for both making the content and bringing in students, without accidentally double-paying or forcing a choice between the two roles. ## What to verify in the admin After you've run a test purchase through your own site, here's what to check: - Open Siren > Conversions and confirm a new conversion appears for the order. If it's stuck in "depending," the underlying order probably hasn't reached completed status in your commerce plugin yet. - Click into the transaction to see the line items, totals, and the calculated payout amount across all programs that fired. - Click the obligation ID to confirm the correct collaborator got credit and the amount matches what your program's incentive structure should produce. - If you expected multiple conversions (like the scenarios above) and only see one, double-check your program groups. A program you expected to fire independently might be grouped with another one by mistake. Running through a test purchase yourself is the fastest way to catch setup problems before real customers and real money are involved. A private browser window, a collaborator's referral link, and a cheap test product are usually all you need. ## Related reading - [What is an affiliate program?](/documentation/general/what-is-an-affiliate-program) - [What are programs?](/documentation/general/what-are-programs) - [What are program groups?](/documentation/general/what-are-program-groups) - [What is a conversion?](/documentation/general/what-is-a-conversion) - [What is an engagement?](/documentation/general/what-is-an-engagement) - [What are transactions?](/documentation/general/what-are-transactions) - [What are obligations?](/documentation/general/what-are-obligations) - [What is a collaborator?](/documentation/general/what-is-a-collaborator) - [Newest engagement wins](/documentation/program-group-structures/newest-engagement-wins) - [Choosing a program group structure](/documentation/program-group-structures/choosing-a-program-group-structure) - [How to pay collaborators](/documentation/getting-started/how-to-pay-collaborators) ## Customizing the Collaborator Portal Source: https://www.sirenaffiliates.com/documentation/getting-started/customizing-the-collaborator-portal How to match the Collaborator Portal's colors to your brand using the block editor, shortcodes, or WordPress Global Styles. The [Collaborator Portal](/documentation/getting-started/the-collaborator-dashboard) is a standalone, full-page experience that your affiliates and partners see when they log in. It replaces the WordPress theme entirely, which means it won't inherit your theme's fonts or layout. But it does pick up your colors. There are three ways to control the portal's appearance, and they layer on top of each other. Global Styles are the baseline, block-level attributes override those, and shortcode attributes override everything. ## Global Styles (automatic) If your WordPress theme supports Global Styles (most block themes do), the portal automatically reads three values from your theme's style settings: - **Background color** from your theme's background setting - **Text color** from your theme's text setting - **Accent color** from your theme's link or accent setting You don't have to do anything for this to work. If you've customized your theme's colors in **Appearance > Editor > Styles**, the portal picks them up automatically. This is the easiest path if your theme already matches your brand. ## Block editor color controls If you're using the Full Site Editor template (the default on Essentials), you can override colors on the portal block itself: 1. Go to **Appearance > Editor > Templates** 2. Find the **Collaborator Portal** template 3. Select the portal block 4. In the block sidebar, open the **Portal Branding** panel 5. Set **Background**, **Text**, and **Accent** colors 6. Optionally set a **Logo** URL to replace the default sidebar icon with your brand mark These override Global Styles for the portal only, so you can use different colors for the portal than the rest of your site. ## Shortcode color attributes If you're using a classic theme (or prefer shortcodes), the portal shortcode accepts color attributes: ``` [siren_collaborator_portal background="#1a1a2e" foreground="#e0e0e0" accent="#00ffb9" logo="https://example.com/your-logo.png"] ``` All four are optional. Any value you don't specify falls back to Global Styles, then to the portal's defaults. ## What you can and can't customize **Today, you can customize:** - Background color - Text color - Accent color (used for links, active states, and highlights) - Sidebar logo (via block attribute or shortcode) These controls handle most brand matching. The portal uses your site's name in the page title and your site's favicon, so those carry over automatically. **Not currently customizable:** - Sidebar labels. The navigation labels ("Dashboard," "Performance," "Earnings," etc.) aren't configurable beyond what WordPress translation/i18n provides. - Typography. The portal uses its own font stack regardless of your theme's font settings. ## For agencies managing client sites If you're building affiliate programs for clients and want the portal to feel native to their brand, set the client's brand colors and logo via the block editor or shortcode. The portal will match their site's palette and show their logo in the sidebar instead of the default icon. ## Delete Collaborator Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/delete Soft-delete or permanently remove a collaborator record using the two-stage delete pattern. ### Delete Collaborator `DELETE /siren/v1/collaborators/{id}` Uses a **two-stage delete** pattern: 1. If the collaborator's status is not `deleted`, performs a soft delete by setting `status` to `deleted`. Returns `200` with the updated record. 2. If the collaborator's status is already `deleted`, permanently removes the record from the database. Returns `204 No Content`. **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. **Events:** Broadcasts `CollaboratorActionEvent` (action: Delete) after success. ## Delete Collaborator Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/delete Permanently deletes a collaborator group. The Core datastore cascades the member rows. # Delete Collaborator Group `DELETE /siren/v1/collaborator-groups/{id}` Permanently deletes the group. There is no soft-delete stage, so the delete is immediate and irreversible. The Core datastore cascades the member rows. Returns `204 No Content` on success. **Events:** Broadcasts `CollaboratorGroupDeleted`. The event fires before the row is removed so downstream listeners can still hydrate the group's name. **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Delete Conversion Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/delete Soft-delete or permanently remove a conversion record using the two-stage delete pattern. ### Delete Conversion `DELETE /siren/v1/conversions/{id}` Uses a two-stage delete pattern. The first DELETE request performs a soft delete by setting the conversion's status to `deleted` and returns `200` with the updated record. If the conversion is already in `deleted` status, a second DELETE request permanently removes the record from the database and returns `204 No Content`. #### Error Responses Returns `404` if no record exists with that ID, or `500` on a database error. #### Events Broadcasts `ConversionActionEvent` (action: Delete) after success. ## Delete Distributor Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors/delete Deletes a distributor using a two-stage pattern: soft delete first, then permanent removal on a second call. # Delete Distributor `DELETE /siren/v1/distributors/{id}` Uses a **two-stage delete** pattern: 1. If the distributor's status is not `deleted`, performs a soft delete by setting `status` to `deleted`. Returns `200` with the updated record. 2. If the distributor's status is already `deleted`, permanently removes the record from the database. Returns `204 No Content`. **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Delete Fulfillment Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments/delete Deletes a fulfillment using a two-stage soft-delete then permanent-delete pattern. ### Delete Fulfillment `DELETE /siren/v1/fulfillments/{id}` This endpoint uses a two-stage delete pattern. The first DELETE request soft-deletes the fulfillment by setting its status to `failed` and returns `200` with the updated record. If the fulfillment is already in the `failed` state, a second DELETE permanently removes the record from the database and returns `204 No Content`. Returns `404` if no record exists with that ID, or `500` on a database error. ## Delete Obligation Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations/delete Two-stage deletion pattern: soft delete on first request, permanent removal on second. ### Delete Obligation `DELETE /siren/v1/obligations/{id}` Deletion follows a two-stage pattern. The first DELETE request against an obligation that is not already `cancelled` performs a soft delete, setting its status to `cancelled` and returning `200` with the updated record. Sending a second DELETE to an obligation that is already `cancelled` permanently removes the record from the database and returns `204 No Content`. #### Error Responses A `404` is returned if no record exists with the given ID. A `500` indicates a database error. #### Events Broadcasts `ObligationActionEvent` (action: Delete) after success. ## Delete Program Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/delete Deletes a program using a two-stage pattern: soft delete first, then permanent removal on a second call. # Delete Program `DELETE /siren/v1/programs/{id}` Uses a **two-stage delete** pattern: 1. **First DELETE.** If the program's status is not `deleted`, performs a soft delete by setting `status` to `deleted`. Returns `200` with the updated record. 2. **Second DELETE.** If the program's status is already `deleted`, permanently removes the record from the database. Returns `204 No Content`. **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. **Events:** Broadcasts `ProgramActionEvent` (action: Delete) after success. ## Delete Program Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups/delete Permanently deletes a program group. There is no soft-delete stage. # Delete Program Group `DELETE /siren/v1/program-groups/{id}` Permanently deletes a program group. There is no soft-delete stage. Program groups have no status field, so the delete is immediate and irreversible. Returns `204 No Content` on success. **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Dependency Injection Source: https://www.sirenaffiliates.com/documentation/extensions/wp-dependency-injection How Siren replaces WordPress globals and singletons with constructor injection and facades. import CodeTabs from "@/components/content/CodeTabs.astro"; # Dependency Injection If you have built WordPress plugins, you have used global functions and singletons to access services. `global $wpdb`, `get_post()`, `WC()->cart`. These are all forms of service location where you reach for a global to get what you need. Siren replaces this pattern with constructor injection: you declare what your class needs in its constructor, and the DI container provides it automatically. ```php // WordPress: reach for globals and singletons function get_order_total($order_id) { global $wpdb; $result = $wpdb->get_var( $wpdb->prepare("SELECT total FROM {$wpdb->prefix}orders WHERE id = %d", $order_id) ); // Or use a global function $post = get_post($order_id); $total = get_post_meta($order_id, '_order_total', true); return $total; } ``` ```php // Siren: declare dependencies in the constructor class OrderTotalService { protected TransactionDatastore $transactions; public function __construct(TransactionDatastore $transactions) { $this->transactions = $transactions; } public function getOrderTotal(int $transactionId): int { $transaction = $this->transactions->getById($transactionId); return $transaction->getTotal(); } } ``` ## How does constructor injection work? When the DI container creates a class, it reads the constructor's type hints and resolves each dependency. If `OrderTotalService` asks for a `TransactionDatastore`, the container finds the concrete class bound to that interface and instantiates it (resolving its own dependencies recursively). You never call `new` for services. The container handles the entire chain. This works automatically for any class resolved through the container: listeners, transformers, admin services, and anything you fetch with `$this->container->get()`. You just declare what you need and it appears. ```php // The container resolves the full dependency chain $service = $this->container->get(OrderTotalService::class); // OrderTotalService gets TransactionDatastore injected // TransactionDatastore gets its own dependencies injected // ...all the way down ``` ## When should I use facades instead? Facades are static wrappers that give you quick access to container-managed services without needing constructor injection. Siren provides facades for common operations: ```php use Siren\Programs\Core\Facades\Programs; use Siren\Collaborators\Core\Facades\Collaborators; use Siren\Configs\Core\Facades\Configs; use Siren\Extensions\Core\Facades\Extensions; // Quick lookups without constructor injection $program = Programs::getById(42); $collaborator = Collaborators::getCollaboratorFromUserId($userId); $value = Configs::getConfigValue('program', '42', 'maxCommission', '0'); ``` Use facades in theme files, one-off scripts, template files, and prototyping, anywhere you do not have access to the DI container. Use constructor injection in extension classes, listeners, transformers, and services, anywhere the container instantiates the class. The rule of thumb: if your class is resolved through the container (listeners, handlers, services, transformers), use constructor injection. If you are writing code outside the container context (a WordPress hook callback in a theme, a quick CLI script), use facades. ## What does a class with multiple injected services look like? Listeners and services commonly depend on several services at once. The container resolves all of them: ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use PHPNomad\Logger\Interfaces\LoggerStrategy; use Siren\Collaborators\Core\Datastores\Collaborator\Interfaces\CollaboratorDatastore; use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore; class SyncCollaboratorSettings implements CanHandle { protected LoggerStrategy $logger; protected CollaboratorDatastore $collaborators; protected ConfigDatastore $config; public function __construct( LoggerStrategy $logger, CollaboratorDatastore $collaborators, ConfigDatastore $config ) { $this->logger = $logger; $this->collaborators = $collaborators; $this->config = $config; } public function handle(Event $event): void { // All three services are available, fully wired by the container. $collaborator = $this->collaborators->getById($event->getCollaboratorId()); $maxCommission = $this->config->getConfigValue( 'program', (string) $event->getProgramId(), 'maxCommission', '0' ); $this->logger->info("Syncing settings for collaborator {$collaborator->getId()}"); } } ``` No globals, no singletons, no manual instantiation. Every dependency is explicit in the constructor, which makes the class easy to test and easy to understand at a glance. ## Where can I learn more about the container? The [Integration Class](/documentation/extensions/integration-class) guide covers container essentials for extension development, including `getClassDefinitions()` and common gotchas. For the full PHPNomad framework documentation on dependency injection, see [Dependency Injection](https://phpnomad.com/core-concepts/dependency-injection/). ## Distribution Events Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-distributions Domain events for scheduled distributions, allocations, and metric tracking. # Distribution Events Distribution events power Siren's scheduled reward system. Unlike the per-conversion program flow, distributions reward collaborators on a recurring schedule based on aggregate performance over time. The events here span two domains: distributions (`Siren\Distributions\Core\Events`) and metrics (`Siren\Metrics\Core\Events`). Together they form a pipeline that goes from heartbeat tick to metric accumulation to allocation to obligation. ## The distribution pipeline The distribution system works on a heartbeat. A scheduler fires the heartbeat event on a regular interval. That event triggers a check for distributions whose trigger date has passed. Each ready distribution is processed, which creates allocations for qualifying collaborators. Each allocation becomes an obligation, entering the same fulfillment pipeline that program-based conversions use. [DistributionHeartbeatInitialized](/documentation/developer-reference/events-distributions/distribution-heartbeat-initialized) is the scheduler trigger that kicks off distribution processing. It carries no data. When it fires, the distribution system queries for all distributions whose trigger date has passed and processes each one. [DistributionCompleted](/documentation/developer-reference/events-distributions/distribution-completed) fires when a distribution period is processed. The listener that handles this event calculates the reward pool, determines each qualifying collaborator's share based on their accumulated metrics, and creates allocation records. [AllocationCompleted](/documentation/developer-reference/events-distributions/allocation-completed) fires when an individual collaborator's share of a distribution has been calculated and recorded. The allocation's value becomes the obligation amount, which enters the same pending-to-fulfilled lifecycle as program-based obligations. ## Metric events Metrics are the performance indicators that distributions use to calculate each collaborator's share. They accumulate continuously between distribution periods. [MetricsTriggered](/documentation/developer-reference/events-distributions/metrics-triggered) fires when new metric data is recorded. Different trigger strategies produce metrics from different sources: a sale-based strategy fires this event when a sale occurs, a visit-based strategy fires it when a site visit is tracked. The strategy ID tells downstream listeners which strategy is responsible. ## DistributionCompleted Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-distributions/distribution-completed Fires when a distribution period is processed. Carries the Distribution model. # DistributionCompleted `DistributionCompleted` fires when a distribution period is processed. This is the event that does the heavy lifting in the distribution pipeline: it calculates the reward pool, determines each collaborator's share from accumulated metrics, and creates allocation records that feed into the standard fulfillment pipeline. The event ID is `distribution_initialized`. The mismatch between the class name and the event ID is a historical artifact from an earlier version of the system where distributions were initialized and completed in separate steps. The class lives at `Siren\Distributions\Core\Events\DistributionCompleted`. ## What does this event carry? The event carries the `Distribution` model, which contains the distribution's configuration: its reward pool, the programs it covers, the metric strategy it uses to rank collaborators, and the schedule that determines when the next period begins. ```php use Siren\Distributions\Core\Events\DistributionCompleted; public function handle(Event $event): void { $distribution = $event->getDistribution(); // The distribution model provides access to the reward pool, // covered programs, metric strategy, and schedule configuration } ``` ## What happens during processing? When the heartbeat trigger detects a distribution whose period has elapsed, this event fires with the distribution model. Listeners evaluate the accumulated metrics for each collaborator enrolled in the distribution's programs, calculate their proportional share of the reward pool, and create `Allocation` records. Each allocation represents one collaborator's share of the distribution. Once an allocation is created, the system fires `AllocationCompleted` for that collaborator, which feeds their share into the standard obligation and fulfillment pipeline. For the full lifecycle of distribution events and how they connect to the fulfillment pipeline, see the [Distribution Events overview](/documentation/developer-reference/events-distributions). ## DistributionHeartbeatInitialized Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-distributions/distribution-heartbeat-initialized The scheduler trigger that kicks off distribution processing. Carries no data. # DistributionHeartbeatInitialized `DistributionHeartbeatInitialized` is the signal that tells the distribution system to wake up and check for work. It carries no data of its own. When the platform's scheduler fires this event, the distribution system queries for any distributions whose trigger date has passed and begins processing them. The event ID is `distribution_heartbeat_initialized`, and its fully qualified class is `Siren\Distributions\Core\Events\DistributionHeartbeatInitialized`. ## Why is this a separate event? Distributions are scheduled rewards that accumulate over time, so they need a periodic trigger to evaluate whether a distribution period has elapsed. Rather than coupling that trigger to a specific scheduling mechanism, the system defines it as a plain event. WordPress fires it from a cron hook. A custom platform could fire it on any cadence it wants. The distribution logic downstream does not care where the signal came from. ## What happens when it fires? When a listener receives this event, it queries the distributions table for any distributions whose next trigger date is in the past. For each one it finds, it fires a `DistributionCompleted` event that carries the distribution model and begins the actual calculation of collaborator shares. Because the event carries no payload, there is nothing to inspect or modify in a listener. Its purpose is purely as a scheduling entry point. ```php use Siren\Distributions\Core\Events\DistributionHeartbeatInitialized; // Firing the heartbeat from a custom scheduling mechanism $dispatcher->dispatch(new DistributionHeartbeatInitialized()); ``` For the full lifecycle of distribution events and how they connect to the fulfillment pipeline, see the [Distribution Events overview](/documentation/developer-reference/events-distributions). ## Distributions Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributions REST API reference for distributions — read-only listing and retrieval, plus manual metric crediting. # Distributions A distribution represents a scheduled payout from a [distributor](/documentation/resource-reference/distributors). When [obligations](/documentation/resource-reference/obligations) accumulate and a distributor's payout cycle triggers, the system groups those obligations into a distribution with a total value and a trigger date. Each distribution tracks its own status through the fulfillment lifecycle and carries a set of allocations linking it back to the individual obligations it covers. Distributions are read-only through the REST API. They are created internally by the distribution engine when payout conditions are met. The only write action available is crediting a manual metric to a collaborator, which lives on the distributors endpoint. ## The Distribution Object | Field | Type | Description | |---|---|---| | `id` | integer | Unique identifier | | `distributorId` | integer | ID of the distributor this distribution belongs to | | `status` | string | Current distribution status | | `value` | integer | Total value of the distribution, in the smallest currency or points unit | | `triggerDate` | datetime | When the distribution is scheduled to be triggered | | `dateCreated` | datetime | When the distribution was created | | `dateModified` | datetime | When the distribution was last modified | ### Extended Fields (via `?fields=`) These fields are resolved dynamically via the field resolver system and must be explicitly requested: | Field | Type | Description | |---|---|---| | `distributorName` | string | Display name of the associated distributor | | `distributorUnits` | string | Unit label configured on the distributor (e.g., `USD`, `points`) | | `allocationCount` | integer | Number of allocations (obligations) included in this distribution | | `allocations` | object[] | Array of allocation objects, each containing `distributionId`, `obligationId`, and `status` | ## Endpoints All endpoints require authentication and are scoped to the current site. Distributions are read-only through the REST API. The only write action available is crediting a manual metric to a collaborator. See the individual endpoint pages in the sidebar for full request and response details. ## Relationship to Other Resources - **[Distributor](/documentation/resource-reference/distributors)** (parent). Every distribution belongs to a distributor via `distributorId`. The distributor defines the payout rules, units, and metric configuration that ultimately produce distributions. - **[Obligation](/documentation/resource-reference/obligations)** (upstream). Distributions aggregate approved obligations. Each allocation within a distribution references one obligation via `obligationId`. - **Metric** (related). Metrics track [collaborator](/documentation/resource-reference/collaborators) scores against distributors. The credit-metric endpoint creates or updates metric records, which feed into the distribution engine's calculations. ## Distributor Compensation Modeling Source: https://www.sirenaffiliates.com/documentation/distribution-structures/distributor-compensation-modeling How to pick the right pool percentage and metric point values for a fair distributor. Worked examples for moving from flat-rate creator pay to performance-based revenue sharing. Setting up a [distributor](/documentation/general/what-are-distributors) requires picking a pool percentage and assigning point values to each metric you track. The mechanics are straightforward once you've done it a few times, but the first time you do it the numbers can feel arbitrary. This page walks through the math so you can pick values that make sense for your business instead of guessing. ## The two levers A distributor has two configuration decisions that drive payouts. Everything else is secondary. The first is the pool percentage: what fraction of your relevant revenue goes into the distribution pool each period. The second is the point values you assign to each tracked event type. Together they determine how much each collaborator earns, and adjusting either one changes the outcome. Pick the pool percentage first, because it's the budget question. Pick the point values second, because they're the fairness question. ## Picking the pool percentage This is the budget question. Ask it like this: how much am I willing to spend on this distributor each period, and what pool percentage produces that spend? If you currently pay collaborators a fixed total each month, divide that total by your monthly relevant revenue to find the equivalent pool percentage. If your monthly subscription revenue is $50,000 and you currently pay 30 instructors a flat $500 each ($15,000 total), that's a 30% pool. Starting at the equivalent percentage means your total payouts under the new model will roughly match your current spend. Nobody's getting a budget surprise. After you've run the new model for a few periods, adjust the percentage based on how the pool feels relative to performance. If collaborators are earning noticeably less than before and you want to keep them happy, bump the percentage up. If the new model is producing payouts larger than the old budget and you can't sustain that, bring it down. Small adjustments compound quickly because the pool scales linearly with the percentage. ## Picking metric point values This is the fairness question. Different events have different difficulty and different value to your business, and the point values encode that judgment. The trap to avoid is weighting every event the same. If you track lesson completions (easy, frequent) and course completions (harder, rarer), giving them equal weight would over-reward instructors whose students take shorter courses, because short-course instructors rack up more total completions per student. A common starting ratio for an LMS is course completion = 10 points, lesson completion = 1 point. Ten lesson completions equal one course completion in the score. This matches the rough intuition that finishing a course is harder and more valuable than finishing a single lesson, without being so lopsided that lessons stop mattering. Adjust the ratio based on what you actually want to reward. If you care more about retention than throughput, weight course completions higher, maybe 20:1 or 50:1. If you're a podcast network paying based on listener engagement, you might weight a full episode listen at 10 points and a partial listen at 1 point. If you're running a content site, you might weight a paid conversion at 100 points and a free signup at 1 point. Pick a ratio that matches your business, then adjust after the first period based on the actual distribution of payouts. ### Per-layer point values for a cascade-bound distributor When the distributor is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, the fairness levers are the per-layer point values `pointsAtLayer1` through `pointsAtLayer5`. A cascade walks up to five layers from the triggering collaborator and credits each peer with the score configured for its layer, so these values decide how much of a trigger flows to each layer of the chain. A decaying ratio such as 100/50/25 is a common starting point: the nearest layer earns the most and each layer further out earns less. Set a layer to 0 to stop the cascade there. A 0 or negative value on a layer terminates the walk, so configuring `pointsAtLayer1` and `pointsAtLayer2` while leaving the rest at 0 limits credit to the two nearest layers. The per-layer value is paid to each qualifying peer at that layer, not split across the layer, so widen the gap between layers if you want the chain to taper quickly. ## A worked example: moving from flat rate to performance-based A course platform pays 30 instructors $500/month each. Monthly subscription revenue is $50,000. They want to move to a performance-based pool tied to student engagement so that instructors whose content actually gets used earn more. Step 1: pool percentage. Current spend divided by relevant revenue is $15,000 / $50,000 = 30%. Start there. Step 2: metric weights. Course completions matter more than lesson completions. Assign course completion = 10 points, lesson completion = 1 point. This is the starting ratio and can be tuned after the first period. Step 3: calculate the first month under the new model. At the end of the month, imagine three instructors with these engagement totals: - Instructor A: 50 lesson completions + 5 course completions = 50 + 50 = 100 points - Instructor B: 200 lesson completions + 2 course completions = 200 + 20 = 220 points - Instructor C: 10 lesson completions + 0 course completions = 10 points The pool is $50,000 × 30% = $15,000. Total points across all 30 instructors (rolling up everyone, not just these three) comes to 3,300. That makes the per-point payout $15,000 / 3,300 = $4.55. Each instructor's earnings are their score times the per-point rate: - Instructor A: 100 × $4.55 = $455 - Instructor B: 220 × $4.55 = $1,001 - Instructor C: 10 × $4.55 = $45.50 Step 4: compare to the flat-rate model. Under the old system, A, B, and C would all have earned $500. Under the new one, A is slightly under, B is heavily over, and C is heavily under. This is the redistribution you wanted. Instructor B's content is being engaged with more, so they earn more. Instructor C's content barely gets used, so they earn less. The pool is the same size, but it lands where it belongs. ## Communicating the change to collaborators Before you flip the switch, show your collaborators the new model with worked examples. Pick a few real instructors from your data and walk them through what the new payout would have been last month. People handle change much better when they can see the math themselves, and it surfaces feedback early. Run it in parallel for one full period if you can afford to. That means calculating what each collaborator would have earned under the new model, but still paying them the old flat rate for that period. At the end of the period, share the "what it would have been" numbers alongside the actual paycheck. The collaborators who would have earned more get an incentive to keep going. The ones who would have earned less get a heads-up and a chance to ask questions before their income actually changes. Be ready to adjust the metric weights based on what you hear. The first ratio you pick is rarely the final one. ## Iterating After the first one or two periods on the new model, look at the actual payouts and decide if they match your intent. Two patterns are worth watching for. If everyone's earning roughly the same amount, the redistribution didn't actually redistribute. This usually means your weight ratio between high-effort and low-effort events is too small. Try doubling the weight on the high-effort event and see what happens the next period. If a small number of collaborators are eating most of the pool and the rest are earning almost nothing, you've got a winner-take-all dynamic. Check whether your high-weight metric is too easy to game (a single instructor flooding the system with a promotional push, for example) and consider either capping individual earnings or switching to a different distribution structure. If the concentration is persistent and not a one-off, the [Performance Weighted Pool](/documentation/distribution-structures/performance-weighted-pool) may not be the right fit, and you should look at [Choosing a Distribution Structure](/documentation/distribution-structures/choosing-a-distribution-structure) to see the alternatives. For more on how distributors themselves work and how the pool calculation fits into the full event pipeline, see [What are Distributors?](/documentation/general/what-are-distributors). ## Further reading - [Moving creators from flat-rate to performance-based pay](/blog/moving-creators-from-flat-rate-to-performance-based-pay) walks through the human side of the transition, including how to announce the change and handle pushback. - [How much should you pay your affiliates](/blog/how-much-should-you-pay-affiliates) covers the budget side of commission math for programs that use a flat rate instead of a pool. ## DistributorBoundToCollaboratorGroup Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group Fires when a distributor is wired to a collaborator group via the distributor edit endpoint. # DistributorBoundToCollaboratorGroup `DistributorBoundToCollaboratorGroup` fires when an operator wires a distributor to a [collaborator group](/documentation/general/what-are-collaborator-groups). The binding is what makes a distribution period's [cascade](/documentation/general/what-is-a-cascade) traverse the group when it allocates the reward pool. A distributor without a bound group has no cascade to run, regardless of which calc strategy is selected. The event ID is `distributor_bound_to_collaborator_group`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\DistributorBoundToCollaboratorGroup`. It fires after the binding is saved, from the distributor edit endpoint. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the distributor id and the group id that was just bound to it. A distributor can be wired to at most one collaborator group at a time. This event is the distributor twin of [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group): the same payload shape and one-group rule, and the same config pattern with `type=distributor` and `subtype=` in place of the program equivalents. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\DistributorBoundToCollaboratorGroup; class HandleDistributorBinding implements CanHandle { public function handle(Event $event): void { if (!$event instanceof DistributorBoundToCollaboratorGroup) { return; } $distributorId = $event->getDistributorId(); $groupId = $event->getGroupId(); // The next distribution period for this distributor will walk this group } } ``` ## How does it fit? The binding is stored as a config row keyed `(type='distributor', subtype=, configKey='collaboratorGroupId')`. The activity feed records the new pairing. Reporting layers that surface "which distributors point at this group" rebuild off the same event. Switching a distributor to a different group fires this event with the new id, after firing [DistributorUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group) for the previous one. Both fire synchronously in the same request, so the unbind always precedes the bind. If a distribution period runs while the distributor has no bound group, the cascade has nothing to walk, so the period completes with no cascade allocations and raises no error. If the bound group's structure cannot support the selected calc (a cascade calc on a flat group, for example), the calc [fails closed](/documentation/calculation-strategies/cascade-troubleshooting) at run time and credits no one, so a bind-time handler is a good place to catch an incompatible pairing early. ## Related events [DistributorUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group) is the unbind side, and [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group) and [ProgramUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group) are the program-side pair. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## Distributors Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors Distributor configuration and metric types — data model, status lifecycle, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; # Distributors A distributor defines how earned rewards are calculated and paid out to [collaborators](/documentation/resource-reference/collaborators). Each distributor specifies an incentive resolver (the formula for computing reward amounts), a pool resolver (the source of funds), a unit of measurement (typically a currency), and an optional schedule that controls when [distributions](/documentation/resource-reference/distributions) are processed. Distributors are the configuration backbone of the distribution pipeline: [programs](/documentation/resource-reference/programs) reference distributors to determine what collaborators receive when [conversions](/documentation/resource-reference/conversions) are approved. The distribution system runs on a heartbeat. On each tick, Siren checks whether any distributions have reached their trigger date, allocates rewards from the pool to qualifying collaborators, and creates obligations. This is a fundamentally different flow from the per-transaction attribution pipeline. ## The distributor object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Name | `name` | `getName()` | string | Display name | | Description | `description` | `getDescription()` | string | Human-readable description of the distributor's purpose | | Distribution resolver | `distributionResolver` | `getDistributionResolver()` | string | Identifier for the incentive resolver strategy (e.g., the formula used to calculate reward amounts) | | Pool resolver | `distributionPoolResolver` | `getDistributionPoolResolver()` | string | Identifier for the pool resolver strategy (determines the source of distributable funds) | | Status | `status` | `getStatus()` | string | Current status: `active`, `inactive`, or `deleted` | | Units | `units` | `getUnits()` | string | Currency or unit identifier (e.g., `USD`) | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the distributor was created | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the distributor was last modified | ## Status lifecycle | Status | Description | |---|---| | `active` | The distributor is live and will process distributions on schedule. | | `inactive` | The distributor is paused. No distributions are processed, but the configuration is preserved. | | `deleted` | Soft-deleted. A second DELETE call permanently removes the record. The restore bulk action moves the distributor back to inactive for review. | ## Accessing distributor data ```bash # List active distributors curl -X GET "https://your-site.com/wp-json/siren/v1/distributors?status=active&fields=id,name,status,units" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single distributor with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/distributors/3?fields=id,name,status,collaboratorCount" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Distributions\Core\Datastores\Distributor\Interfaces\DistributorDatastore; class DistributorReport { protected DistributorDatastore $distributors; public function __construct(DistributorDatastore $distributors) { $this->distributors = $distributors; } public function getActiveDistributors(): array { return $this->distributors->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); } } ``` ```php use Siren\Distributions\Core\Facades\Distributors; $active = Distributors::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); $distributor = Distributors::getById(3); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Distributions are created and completed automatically by the heartbeat and allocation pipeline. The events `distribution_completed` and `allocation_completed` drive the downstream obligation creation. If you're reading these datastores, you're most likely building reporting or dashboards. If you find yourself creating distributions manually, you're probably working against the heartbeat system rather than with it. ## PHP domain methods ### Looking up a collaborator's distributors `getCollaboratorDistributors` retrieves all distributors a specific collaborator participates in, with pagination support. ```php use Siren\Distributions\Core\Datastores\Distributor\Interfaces\DistributorDatastore; class CollaboratorDashboard { protected DistributorDatastore $distributors; public function __construct(DistributorDatastore $distributors) { $this->distributors = $distributors; } public function listDistributors(int $collaboratorId): array { // Defaults to 10 results starting at offset 0 return $this->distributors->getCollaboratorDistributors($collaboratorId); } public function listAllDistributors(int $collaboratorId): array { return $this->distributors->getCollaboratorDistributors($collaboratorId, 50, 0); } } ``` ```php use Siren\Distributions\Core\Facades\Distributors; $distributors = Distributors::getCollaboratorDistributors($collaboratorId); // With pagination $distributors = Distributors::getCollaboratorDistributors($collaboratorId, 25, 0); ``` ### Counting collaborators in a distributor `getCollaboratorCount` returns the estimated number of collaborators enrolled in a given distributor. This method is available on the datastore interface through dependency injection. ```php $count = $this->distributors->getCollaboratorCount($distributorId); ``` ## Extended fields (REST only) ### Via `?fields=` | Field | Type | Description | |---|---|---| | `collaboratorCount` | integer | Number of collaborators associated with this distributor | ### Specialized fields (single-record only) These fields are always included in the single-record response (`GET /siren/v1/distributors/{id}`) and are not part of the field resolver system: | Field | Type | Description | |---|---|---| | `schedule` | string[] or null | Array of DateTime modifier strings that control when distributions are processed (e.g., `["first day of next month"]`) | | `engagementTypes` | object[] | Metric types configured for this distributor, each with `id`, `type`, and `value` | | `transactionCompilers` | string[] | Identifiers for the transaction compilers used when building distribution data | | `revenuePercentage` | integer or null | Revenue percentage used by the pool resolver, if configured | | `lineItemFilters` | object or null | Filtering rules applied to transaction line items during distribution | | `currentDistribution` | object or null | The currently active distribution record (available via `include=extended` or by requesting the field explicitly) | ## Metric types Metric types define which performance indicators a distributor tracks and how they're weighted. Each metric type record links a distributor to a specific metric identifier and its current accumulated value. ### The metric type model | Field | PHP getter | Type | Description | |---|---|---|---| | ID | `getId()` | integer | Primary key | | Distributor | `getDistributorId()` | integer | The distributor this metric belongs to | | Metric type | `getMetricType()` | string | Metric type identifier (e.g., `totalSales`, `referralCount`) | | Metric value | `getMetricValue()` | integer | Current accumulated value for this metric | ### Accessing metric type data ```php use Siren\Distributions\Core\Datastores\DistributorMetricTypes\Interfaces\DistributorMetricTypeDatastore; class MetricInspector { protected DistributorMetricTypeDatastore $metricTypes; public function __construct(DistributorMetricTypeDatastore $metricTypes) { $this->metricTypes = $metricTypes; } public function getAllMetrics(int $distributorId): array { return $this->metricTypes->getDistributorMetricTypes($distributorId); } } ``` ```php use Siren\Distributions\Core\Facades\DistributorMetricTypes; $metrics = DistributorMetricTypes::getDistributorMetricTypes($distributorId); foreach ($metrics as $metric) { echo $metric->getMetricType() . ': ' . $metric->getMetricValue(); } ``` ### Metric type domain methods `getMetricTypeForDistributor` retrieves a single metric type record by distributor ID and metric type identifier. Throws `RecordNotFoundException` if the distributor doesn't have the specified metric configured. ```php use Siren\Distributions\Core\Facades\DistributorMetricTypes; $salesMetric = DistributorMetricTypes::getMetricTypeForDistributor($distributorId, 'totalSales'); echo 'Total sales metric value: ' . $salesMetric->getMetricValue(); ``` `getDistributorMetricTypes` returns all metric type records configured for a given distributor. This is useful for building summary views that show all performance indicators at once. ```php use Siren\Distributions\Core\Facades\DistributorMetricTypes; $metrics = DistributorMetricTypes::getDistributorMetricTypes($distributorId); foreach ($metrics as $metric) { echo $metric->getMetricType() . ': ' . $metric->getMetricValue(); } ``` ## Relationships - **[Program](/documentation/resource-reference/programs)** (upstream). Programs reference one or more distributors to define what [collaborators](/documentation/resource-reference/collaborators) earn. A single distributor can serve multiple programs. - **[Distribution](/documentation/resource-reference/distributions)** (downstream). When a distribution cycle runs, the distributor produces distribution records that track the calculated amounts owed to each collaborator. - **[Obligation](/documentation/resource-reference/obligations)** (downstream, indirect). Distributions feed into the obligation and [fulfillment](/documentation/resource-reference/fulfillments) pipeline, ultimately resulting in payouts. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## DistributorUnboundFromCollaboratorGroup Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group Fires when a distributor's collaborator group binding is cleared. # DistributorUnboundFromCollaboratorGroup `DistributorUnboundFromCollaboratorGroup` fires when an operator clears a distributor's [collaborator group](/documentation/general/what-are-collaborator-groups) binding. After this event, the distributor still runs its scheduled distribution periods, but a cascade-style allocation calc now has no group to walk. It [fails closed](/documentation/calculation-strategies/cascade-troubleshooting): the period allocates nothing to anyone and raises no error, so the distributor keeps running periods that pay no one until it is bound to a group again. That silent zero-payout is the main reason to handle this event. The event ID is `distributor_unbound_from_collaborator_group`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\DistributorUnboundFromCollaboratorGroup`. It fires after the binding is cleared, from the distributor edit endpoint. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the distributor id and the group id that the distributor was previously bound to. The previous group id stays in the payload so audit logs can describe the change as "unbound from group #42" rather than "unbound from something." ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\DistributorUnboundFromCollaboratorGroup; class HandleDistributorUnbinding implements CanHandle { public function handle(Event $event): void { if (!$event instanceof DistributorUnboundFromCollaboratorGroup) { return; } $distributorId = $event->getDistributorId(); $previousGroupId = $event->getGroupId(); // The next distribution period for this distributor has no group to walk } } ``` ## How does it fit? The unbind only broadcasts when there was a real prior binding to clear, so idempotent edits don't generate phantom activity entries. Re-binding to a different group produces an unbind for the old group id followed immediately by a [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group) for the new one. Both fire synchronously in the same request, so a transition listener always sees the unbind before the bind. This event tracks the deliberate unbind or rebind action, not the liveness of the binding. Deleting the bound group [does not fire it](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted): the binding row stays on the distributor, now pointing at a group that is gone, and a cascade bound to it fails closed. Deleting the distributor does not fire it either. So a system that mirrors which distributors point at which groups must reconcile group and distributor deletions on its own rather than rely on this event alone. ## Related events [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group) is the bind side, and [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group) and [ProgramUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group) are the program-side pair. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## Documentation Source: https://www.sirenaffiliates.com/documentation/welcome Everything you need to set up, manage, and extend Siren. import Card from "@/components/ui/Card.astro"; # Welcome to the Siren documentation Siren is an incentive program management system for WordPress. It handles the full lifecycle of affiliate, referral, and reward programs, from tracking customer referrals through calculating commissions to paying collaborators. This documentation covers everything from initial setup through deep technical reference. Pick the section that matches where you are.

Getting Started

New to Siren? Learn what it does and build your first program.

What is Siren? →

User Guide

Programs, collaborators, engagements, conversions, and how rewards are calculated.

Core concepts →

Recipes

Pre-built incentive program configurations you can install in one click and customize from there.

Browse recipes →

Developer Reference

Domain models, datastores, and the end-to-end attribution pipeline.

Developer intro →

API Reference

REST endpoints for programs, collaborators, conversions, and more.

API intro →

Extension Development

Build your own Siren extensions with custom integrations and strategies.

Architecture overview →

Migrating to Siren

Switching from another affiliate plugin? Guides for AffiliateWP, SliceWP, Easy Affiliate, and Solid Affiliate.

Migration overview →
## Downline cascade calculation Source: https://www.sirenaffiliates.com/documentation/calculation-strategies/downline-cascade Walk the chain below the triggering collaborator and emit per-layer scores. Downline Cascade credits the collaborators below the triggering collaborator. When the trigger fires, Siren walks down the bound [collaborator group](/documentation/general/what-are-collaborator-groups) and emits one score per layer at the configured per-layer amount. ## How it works A cascade starts from the triggering collaborator's position in the group and walks toward the leaves. The triggering collaborator is never credited, only the people below them. Each layer is 1-indexed. Layer 1 is the direct downline: the next position down in a [linear chain](/documentation/collaborator-group-structures/linear-chain), or the immediate children in a [parent-child](/documentation/collaborator-group-structures/parent-child) tree. Layer 2 is the layer below that. The maximum is layer 5. For each layer the cascade visits, Siren reads `pointsAtLayer{N}` from the calc's configured args and emits one credit per peer at that layer for that amount. In a parent-child tree, layer 1 can include several peers. Every direct child of the trigger gets a layer-1 credit. The per-layer score is per peer, not split across the layer. A 0 (or unset) value at any layer stops the cascade at that point. If `pointsAtLayer3` is 0, layers 4 and 5 never run, even if they're configured. Layers are counted by distance from the trigger across the whole tree, so a 0 at a layer stops every branch at that depth, not just one. The per-layer value is a score, not a dollar amount. It sets each collaborator's weight, and the program's incentive turns those weights into the actual payout. Pairing the cascade with a [performance-weighted pool](/documentation/program-structures/performance-weighted-pool) splits a reward pool in proportion to the scores, so the layer values decide each person's share of that pool. Inactive peers (suspended, deleted, or otherwise excluded) are skipped without consuming the layer slot. Other peers at the same layer still get credited at the per-layer rate. The results flow through the same engagement and metric pipelines as [Fixed](/documentation/calculation-strategies/fixed). Downstream attribution sees a set of credits instead of one, but the shape is identical. If the bound collaborator group does not expose layers (a flat group, for example), Downline Cascade has nothing to walk and fails closed. It emits nothing and never falls back to a single Fixed credit. The picker hides Downline Cascade when a flat group is bound, so you should not reach this state in normal use, but the picker is the only guard. If a cascade is already saved and the bound group is later changed to flat, deleted, or unbound, the calc keeps the cascade strategy and pays out zero rather than reverting to Fixed. See [Cascade troubleshooting](/documentation/calculation-strategies/cascade-troubleshooting) for the states that produce zero payouts after configuration. ## When to use it Use Downline Cascade when the people below the trigger should share the payout. Common cases: - Awarding a bonus to a team's members when their team lead hits a milestone. - Team performance payouts where a manager closing a deal pushes a slice to each team member. - Override-style bonuses where a result at the top of a team distributes a share to each level beneath it. Downline is less common than [Upline Cascade](/documentation/calculation-strategies/upline-cascade), but it fits compensation plans that reward leadership through team distribution, when the goal is to motivate the layers under a top performer rather than the layers above a producer. If you're not sure which direction you want, read [Choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy). ## Configuration Five integer args: `pointsAtLayer1`, `pointsAtLayer2`, `pointsAtLayer3`, `pointsAtLayer4`, `pointsAtLayer5`. Each one sets the score for one peer at that layer. A 0 at any layer stops the cascade at that point. Set layers you don't want to use to 0. The picker on the Programs Edit and Distributors Edit screens only offers Downline Cascade when the bound collaborator group provides layered walker steps. Flat groups don't, so the option stays hidden. Linear chain and parent-child both qualify. See [Why some calculation methods disappear](/documentation/general/calc-capability-matching) for the matching logic. The program side and the distributor side differ in where the bound group and the args live. On the program side, the bound group is read from the program config (config type `program`, key `collaboratorGroupId`) and the per-layer args are stored on the program engagement type (config type `programEngagementTypeArg`). On the distributor side, the bound group is read from the distributor config (config type `distributor`, key `collaboratorGroupId`) and the per-layer args come from the distributor metric type (config type `distributorMetricTypeArg`). The arg keys are the same on both sides (`pointsAtLayer1` through `pointsAtLayer5`), so a metric-side cascade needs a group bound to the distributor and is configured on the distributor's metric type. ## Worked example: chain A 4-person [linear chain](/documentation/collaborator-group-structures/linear-chain), ordered top to bottom: - chain-one, position 1 - chain-two, position 2 - chain-three, position 3 - chain-four, position 4 The program uses Downline Cascade with `pointsAtLayer1: 100, pointsAtLayer2: 50, pointsAtLayer3: 25, pointsAtLayer4: 0`. chain-one makes a sale. The cascade starts at chain-one and walks down. chain-two is layer 1 and gets a score of 100. chain-three is layer 2 and gets a score of 50. chain-four is layer 3 and gets a score of 25. Layer 4 is 0, so the cascade stops. chain-one, the seller, gets no score from this calc. ## Worked example: tree A 7-person [parent-child](/documentation/collaborator-group-structures/parent-child) tree: - alex (root) - rae (child of alex) - ivy (child of rae) - jude (child of rae) - kit (child of rae) - ravi (child of alex) The program uses Downline Cascade with `pointsAtLayer1: 60, pointsAtLayer2: 30, pointsAtLayer3: 0`. alex makes a sale. The cascade walks down from alex. Layer 1 is the direct children, rae and ravi, and each gets a score of 60. Layer 2 is rae's children: ivy, jude, and kit each get a score of 30. Layer 3 is 0, so the cascade stops. alex gets no score from this calc. This is why per-layer values are described as "per peer per layer" rather than "per layer total." A layer with five peers emits five separate scores at the per-layer value. If you need a refresher on the underlying mechanic, see [What is a cascade](/documentation/general/what-is-a-cascade). ## EngagementAwarded Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-attribution/engagement-awarded Fires when an individual engagement is awarded credit during the conversion process. # EngagementAwarded Creating an engagement is not the same as awarding it credit. When `EngagementsTriggered` fires, engagement records are created and sit in an active state, waiting. Later, when a sale or lead comes through, the conversion system evaluates which engagements should receive credit based on the program's resolver strategy (shared pool, top score wins, and so on). Each engagement that receives credit produces an `EngagementAwarded` event. The event is identified as `Engagement_awarded` and lives in the `Siren\Engagements\Core\Events` namespace. ## What does this event carry? This event carries richer data than most attribution events because it needs to communicate the full reward context. Listeners receive the `Engagement` model, an optional `Transaction` model, an `Incentive` instance that describes the reward structure, and an `IncentiveResolver` that can calculate the actual reward amount. ```php use Siren\Engagements\Core\Events\EngagementAwarded; $engagement = $event->getEngagement(); $transaction = $event->getTransaction(); $incentiveType = $event->getIncentiveType(); $resolver = $event->getIncentiveResolver(); ``` The transaction is optional because not every conversion involves a financial transaction. Lead-based programs award engagements without a sale, so `getTransaction()` returns null in those cases. The incentive and resolver together provide the complete calculation context. The `Incentive` describes the reward structure (percentage-based, flat rate, tiered), while the `IncentiveResolver` can compute the actual dollar amount the collaborator earns. This saves listeners from re-deriving the reward logic themselves. ## What happens when it fires? Because each engagement is awarded individually, a single conversion can produce multiple `EngagementAwarded` events if multiple engagements receive credit. This happens when a program uses a shared-pool resolver that splits credit among several collaborators, or when a customer's purchase spans multiple programs. See the [Attribution Events overview](/documentation/developer-reference/events-attribution) for how this event fits into the full attribution pipeline. ## EngagementCompleted Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-attribution/engagement-completed Fires when engagements for an opportunity finish the conversion process. # EngagementCompleted Once the conversion system has finished evaluating and awarding credit for all engagements tied to an opportunity, the system fires `EngagementCompleted` to signal that attribution processing is done. This is the final event in the engagement lifecycle, transitioning engagements from active to completed status. The event is identified as `engagement_completed` and lives in the `Siren\Engagements\Core\Events` namespace. ## What does this event carry? The event carries an `opportunityId` and a `conversionType` string that describes what kind of conversion occurred. ```php use Siren\Engagements\Core\Events\EngagementCompleted; $opportunityId = $event->getOpportunityId(); $conversionType = $event->getConversionType(); ``` Rather than carrying individual engagement models, this event works at the opportunity level. All engagements associated with the opportunity are affected when this event fires, which reflects how the conversion system processes engagements in batches rather than one at a time. ## What happens when it fires? The `EndOpportunityEngagements` listener marks all engagements for the opportunity as completed. Completed engagements no longer participate in future conversion evaluations, which prevents double-counting if the same customer makes additional purchases. ## Coordination events that bridge into conversions Two related events handle the internal handoff between the attribution and conversion layers. These are coordination events rather than lifecycle events, and most integrations will not need to listen to them directly. `EngagementInitialized` (event ID: `Engagement_initialized`, namespace `Siren\Engagements\Core\Events`) fires when the system begins processing a sale or lead for conversion. It carries an `opportunityId`, an array of `transactionDetails` containing line items from the commerce event, and a `type` string identifying the engagement type. This event signals the conversion system to prepare for building conversion records. `TransactionTriggered` (event ID: `transaction_triggered`, namespace `Siren\Engagements\Core\Events`) fires when the attribution system produces the data needed for a transaction record. It carries an `opportunityId` and the `transactionDetails` array. This is the handoff point where line item data flows from the commerce layer into the engagement system. When it fires, `InitializeSaleEngagement` picks it up and begins evaluating which engagement trigger strategies apply. These two events exist to decouple the commerce layer from the attribution layer. Commerce events produce raw sale data; `TransactionTriggered` and `EngagementInitialized` translate that data into the format the engagement system expects, keeping each layer focused on its own concerns. See the [Attribution Events overview](/documentation/developer-reference/events-attribution) for how this event fits into the full attribution pipeline. ## Engagements Source: https://www.sirenaffiliates.com/documentation/resource-reference/engagements Attribution engagements — data model, status lifecycle, REST API, and PHP data access for bindings, scoring, and opportunity lookups. import CodeTabs from "@/components/content/CodeTabs.astro"; import FlowchartSteps from "@/components/blocks/FlowchartSteps.astro"; # Engagements An engagement represents a confirmed attribution event linking a [collaborator](/documentation/resource-reference/collaborators) to an opportunity within a specific [program](/documentation/resource-reference/programs). When a visitor arrives through a collaborator's referral link, uses a bound coupon code, or is manually attributed, Siren creates an engagement to record that the collaborator participated. Engagements sit upstream of [conversions](/documentation/resource-reference/conversions) in the [attribution pipeline](/documentation/resource-reference/pipeline-overview): an engagement captures the collaborator's involvement, and when the opportunity converts, the engagement feeds into the conversion record. Each engagement carries a score that reflects its attribution weight. When multiple engagements exist for the same opportunity and program (for example, repeated visits), Siren merges them and accumulates the score. This merged score is what ultimately appears on any conversion produced from the engagement. Engagements are read-only through the REST API. They are created and managed internally by the engagement trigger system in response to events like site visits, coupon usage, and manual attribution. ## The engagement object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Opportunity | `opportunityId` | `getOpportunityId()` | integer | The opportunity this engagement is linked to | | Program | `programId` | `getProgramId()` | integer | The program under which this engagement was attributed | | Collaborator | `collaboratorId` | `getCollaboratorId()` | integer | The collaborator who receives credit | | Score | `score` | `getScore()` | integer | Attribution score representing the weight of this engagement | | Status | `status` | `getStatus()` | string | Lifecycle status: `active`, `pending`, or `complete` | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the engagement was first recorded | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the engagement was last updated | ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested: | Field | Type | Description | |---|---|---| | `collaboratorName` | string | Display name of the collaborator who triggered this engagement | | `programName` | string | Name of the program this engagement belongs to | ### The EngagementType object Engagement types define the trigger strategies that can create engagements. Each type represents a different mechanism for detecting collaborator involvement. | Field | Type | Description | |---|---|---| | `id` | string | Unique type identifier (e.g., `referredSiteVisit`, `boundCouponUsed`) | | `name` | string | Human-readable display name | The core trigger strategies are: - `referredSiteVisit`. Fires when a visitor arrives through a collaborator's referral link. This is the most common engagement type. - `boundCouponUsed`. Fires when a coupon code bound to a collaborator is applied to an order. Only available when a coupon-supporting integration is active. - `manual`. Fires when a manager manually attributes a transaction to a collaborator. ## Status lifecycle | Status | Description | |---|---| | `active` | The engagement is live. The collaborator's referral is currently being tracked against the linked opportunity. New engagements always start in this state. | | `pending` | The opportunity has triggered a conversion type, and the engagement is waiting for conversion processing. Active engagements transition to pending when the associated opportunity completes for a matching conversion type. | | `complete` | Conversions have been awarded for this engagement's opportunity and program. The engagement's attribution work is done. This transition happens automatically when the ConversionsAwarded event fires. | ## Accessing engagement data ```bash # List engagements for a specific collaborator curl -X GET "https://your-site.com/wp-json/siren/v1/engagements?collaboratorId=42&fields=id,score,status,programName" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single engagement with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/engagements/15?fields=id,score,status,collaboratorName,programName" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Engagements\Core\Datastores\Engagement\Interfaces\EngagementDatastore; class AttributionReport { protected EngagementDatastore $engagements; public function __construct(EngagementDatastore $engagements) { $this->engagements = $engagements; } public function getActiveForOpportunity(int $opportunityId): array { return $this->engagements->andWhere([ ['column' => 'opportunityId', 'operator' => '=', 'value' => $opportunityId], ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); } } ``` ```php use Siren\Engagements\Core\Facades\Engagements; $active = Engagements::andWhere([ ['column' => 'opportunityId', 'operator' => '=', 'value' => $opportunityId], ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); $engagement = Engagements::getById(42); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Engagements are created automatically when attribution triggers fire. If you find yourself writing code that creates engagement records directly, consider whether your use case would be better served by registering a custom engagement trigger strategy so the normal pipeline handles it. ## PHP domain methods ### Retrieving bindings for an opportunity and program `getBindings` returns all active engagements for a given opportunity and program combination. This is the primary method for inspecting which collaborators are currently credited within a specific program context. Results can be sorted and limited. The method signature is `getBindings(int $opportunityId, int $programId, string $orderBy = 'dateCreated', string $order = 'ASC', ?int $limit = null)`. Only active engagements are returned. The method filters by `status = 'active'` internally. ```php use Siren\Engagements\Core\Datastores\Engagement\Interfaces\EngagementDatastore; class BindingInspector { protected EngagementDatastore $engagements; public function __construct(EngagementDatastore $engagements) { $this->engagements = $engagements; } public function listBindings(int $opportunityId, int $programId): array { // Returns Engagement[] sorted by dateCreated ascending return $this->engagements->getBindings($opportunityId, $programId); } public function getRecentBindings(int $opportunityId, int $programId): array { // Sort by dateCreated descending, limit to 5 return $this->engagements->getBindings( $opportunityId, $programId, 'dateCreated', 'DESC', 5 ); } } ``` ```php use Siren\Engagements\Core\Facades\Engagements; // All active bindings for this opportunity + program, oldest first $bindings = Engagements::getBindings($opportunityId, $programId); // Most recent 3 bindings $recent = Engagements::getBindings($opportunityId, $programId, 'dateCreated', 'DESC', 3); ``` ### Getting the newest binding `getNewestBinding` returns the single most recently modified active engagement for an opportunity and program pair. This is useful when you need to know which collaborator was most recently attributed within a specific program context. Throws `RecordNotFoundException` if no active bindings exist. ```php try { $newest = $this->engagements->getNewestBinding($opportunityId, $programId); $collaboratorId = $newest->getCollaboratorId(); } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { // No active bindings exist for this combination } ``` ```php use Siren\Engagements\Core\Facades\Engagements; $newest = Engagements::getNewestBinding($opportunityId, $programId); echo $newest->getCollaboratorId(); // The most recently credited collaborator ``` ### Getting the oldest engagement `getOldestEngagement` returns the earliest active engagement for an opportunity and program pair, sorted by creation date. This is the counterpart to `getNewestBinding`. Use it when first-touch attribution matters. Throws `RecordNotFoundException` if no active bindings exist. ```php try { $oldest = $this->engagements->getOldestEngagement($opportunityId, $programId); $firstTouch = $oldest->getCreatedDate(); } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { // No active bindings exist } ``` ```php use Siren\Engagements\Core\Facades\Engagements; $oldest = Engagements::getOldestEngagement($opportunityId, $programId); echo $oldest->getCollaboratorId(); // The first collaborator attributed ``` ### Counting active engagements `getActiveEngagementCount` returns the number of active engagements for an opportunity and program pair. This is a lightweight way to check whether any attribution exists without loading full model instances. ```php $count = $this->engagements->getActiveEngagementCount($opportunityId, $programId); if ($count > 0) { // At least one collaborator is actively credited } ``` ```php use Siren\Engagements\Core\Facades\Engagements; $count = Engagements::getActiveEngagementCount($opportunityId, $programId); ``` ### Finding a specific active engagement `getActiveEngagement` retrieves a single active engagement by collaborator, program, and opportunity. Use this when you need to confirm that a specific collaborator currently holds attribution for a particular opportunity within a particular program. The method signature is `getActiveEngagement(int $collaboratorId, int $programId, int $opportunityId)`. Note that the parameter order is different here — collaborator comes first, unlike most other engagement methods where opportunity is first. Throws `RecordNotFoundException` if no matching active engagement exists. ```php use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $engagement = $this->engagements->getActiveEngagement( $collaboratorId, $programId, $opportunityId ); // This collaborator is actively credited $score = $engagement->getScore(); } catch (RecordNotFoundException $e) { // No active engagement for this collaborator/program/opportunity combination } ``` ```php use Siren\Engagements\Core\Facades\Engagements; $engagement = Engagements::getActiveEngagement($collaboratorId, $programId, $opportunityId); ``` ### Listing all engagements for an opportunity `getEngagementsForOpportunity` returns every engagement associated with a given opportunity, regardless of status. This is broader than `getBindings` because it includes active, pending, complete, and cancelled engagements. This is particularly useful for building audit trails or attribution history views. ```php $allEngagements = $this->engagements->getEngagementsForOpportunity($opportunityId); foreach ($allEngagements as $engagement) { echo $engagement->getCollaboratorId() . ': ' . $engagement->getStatus(); } ``` ```php use Siren\Engagements\Core\Facades\Engagements; $allEngagements = Engagements::getEngagementsForOpportunity($opportunityId); // Filter completed engagements $completed = array_filter($allEngagements, function ($e) { return $e->getStatus() === 'complete'; }); ``` ## Access control (REST) All endpoints require authentication and are scoped to the current site. Collaborators can only see their own engagements; administrators can see all records. ## Relationships - **Opportunity** (upstream). Every engagement is linked to an opportunity via `opportunityId`. The opportunity represents the trackable event (such as a site visit or coupon usage) that the collaborator participated in. - **[Program](/documentation/resource-reference/programs).** Every engagement belongs to a program via `programId`. The program defines the rules under which the engagement was created, including which trigger strategies are enabled. - **[Collaborator](/documentation/resource-reference/collaborators).** Every engagement is linked to a collaborator via `collaboratorId`. The collaborator is the partner whose referral activity produced this engagement. - **[Conversion](/documentation/resource-reference/conversions)** (downstream). When an opportunity converts, the system uses the engagement to create a conversion record. The conversion references the engagement via its `engagementId` field. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## EngagementsTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-attribution/engagements-triggered Fires when engagement trigger strategies produce new engagement records for an opportunity. # EngagementsTriggered After an opportunity is created, engagement trigger strategies evaluate it and decide which collaborator-program combinations should receive engagement records. Once those records are created, the system fires `EngagementsTriggered` to signal that new engagements exist and are ready for downstream processing. The event is identified as `engagements_triggered` and lives in the `Siren\Engagements\Core\Events` namespace. ## What does this event carry? The event carries an array of `Engagement` models and a `strategyId` string that identifies which trigger strategy created them. ```php use Siren\Engagements\Core\Events\EngagementsTriggered; $engagements = $event->getEngagements(); $strategyId = $event->getTriggerStrategyId(); ``` This event always carries an array rather than a single engagement. A single opportunity can produce multiple engagements when a collaborator participates in more than one program. If a collaborator is enrolled in both a revenue-share program and a flat-rate bonus program, the trigger strategy creates an engagement for each, and all of them arrive together in this event. The strategy ID tells you which trigger strategy produced these engagements. Different strategies handle different referral mechanisms (site visits, coupons, manual attribution), so the strategy ID provides context about how the attribution was determined. ## What happens when it fires? The reporting system picks up this event and updates activity period records, ensuring that dashboards and reports reflect the new engagement activity without waiting for a conversion to occur. This is what allows collaborators to see their referral traffic in real time, before any sales have happened. Engagements created by this event exist in an active state, waiting for a conversion to award them credit. That next step happens through [EngagementAwarded](/documentation/developer-reference/events-attribution/engagement-awarded), which fires later when the conversion system determines which engagements deserve credit for a sale or lead. See the [Attribution Events overview](/documentation/developer-reference/events-attribution) for how this event fits into the full attribution pipeline. ## Enums & Constants Reference Source: https://www.sirenaffiliates.com/documentation/resource-reference/enums-and-constants Complete reference of feature flags, status values, currencies, engagement triggers, incentive types, resolvers, and other constants used throughout Siren. ## Feature flags The `Features` constants control which capabilities an extension declares support for. Extensions return these from their `getSupports()` method to tell Siren which engagement triggers and conversion types they enable. | Constant | Value | What it enables | |---|---|---| | `Features::Coupons` | `coupons` | Coupon-based engagement tracking | | `Features::Courses` | `courses` | Course completion engagement triggers | | `Features::Lessons` | `lessons` | Lesson completion engagement triggers | | `Features::Posts` | `posts` | Blog post visit engagement triggers | | `Features::Renewals` | `renewals` | Renewal conversion type | | `Features::Forms` | `forms` | Form submission engagement triggers | | `Features::ManualOrdering` | `manual_ordering` | Manual product ordering support | **Source:** `Siren\Extensions\Core\Enums\Features` ### Frontend feature flags The frontend build system uses a separate set of flags to control which admin screens are available. These are injected at build time and cannot be changed at runtime. | Flag | Lite | Essentials | |---|---|---| | `programs` | yes | yes | | `collaborators` | yes | yes | | `conversions` | yes | yes | | `engagements` | yes | yes | | `opportunities` | yes | yes | | `obligations` | yes | yes | | `fulfillments` | yes | yes | | `transactions` | yes | yes | | `programGroups` | no | yes | | `distributors` | no | yes | ## Status values ### Collaborator status | Status | Description | |---|---| | `active` | Participating in programs, earning rewards | | `pending` | Awaiting approval | | `inactive` | Temporarily disabled | | `rejected` | Application denied | | `deleted` | Soft-deleted | ### Program status | Status | Description | |---|---| | `active` | Running and processing conversions | | `inactive` | Paused, not processing | | `draft` | Not yet published | | `deleted` | Soft-deleted | ### Conversion status | Status | Description | |---|---| | `pending` | Awaiting commerce event completion | | `approved` | Confirmed, obligation being calculated | | `complete` | Fully processed with obligation created | | `rejected` | Denied (e.g., refund) | | `deleted` | Soft-deleted | ### Engagement status | Status | Description | |---|---| | `active` | Credit claim is live, awaiting conversion | | `complete` | Converted into an obligation | | `expired` | Engagement window elapsed | ### Obligation status | Status | Description | |---|---| | `pending` | Awaiting approval or fulfillment | | `approved` | Confirmed, ready for payout | | `complete` | Included in a fulfillment and paid | | `rejected` | Cancelled or denied | | `deleted` | Soft-deleted | ### Fulfillment status | Status | Description | |---|---| | `pending` | Created, not yet processing | | `processing` | Payment in progress | | `complete` | All payouts disbursed | | `failed` | Payment processing failed | ### Payout status | Status | Description | |---|---| | `paid` | Disbursed to collaborator | | `unpaid` | Awaiting payment | ### Transaction status | Status | Description | |---|---| | `pending` | Commerce event received, not finalized | | `complete` | Order confirmed and finalized | | `failed` | Transaction did not complete | ### Distributor status | Status | Description | |---|---| | `active` | Running distributions on schedule | | `inactive` | Paused | | `deleted` | Soft-deleted | ## Currencies Siren ships with the following currencies registered. Extensions can register additional currencies via the `CurrencyRegistryInitiated` event. | Code | Symbol | Name | |---|---|---| | `USD` | $ | US Dollar | | `EUR` | € | Euro | | `GBP` | £ | British Pound | | `JPY` | ¥ | Japanese Yen | | `CAD` | $ | Canadian Dollar | | `AUD` | $ | Australian Dollar | | `NZD` | $ | New Zealand Dollar | | `CHF` | CHF | Swiss Franc | | `CNY` | ¥ | Chinese Yuan | | `INR` | ₹ | Indian Rupee | | `BRL` | R$ | Brazilian Real | | `MXN` | $ | Mexican Peso | | `SGD` | $ | Singapore Dollar | | `HKD` | $ | Hong Kong Dollar | | `KRW` | ₩ | South Korean Won | | `SEK` | kr | Swedish Krona | | `NOK` | kr | Norwegian Krone | | `PLN` | zł | Polish Zloty | | `TRY` | ₺ | Turkish Lira | | `THB` | ฿ | Thai Baht | | `MYR` | RM | Malaysian Ringgit | | `ZAR` | R | South African Rand | | `RUB` | ₽ | Russian Ruble | All monetary values in Siren are stored as integers in the smallest currency unit (e.g., cents for USD). See [Amount & Currency](/documentation/extensions/amount-currency) for conversion utilities. ## Engagement trigger types Engagement triggers define what actions create credit claims for collaborators. ### Core triggers (always available) | ID | Label | Description | |---|---|---| | `referredSiteVisit` | Site Visited | Customer arrives via a collaborator's referral link | | `manual` | Manual Attribution | Admin manually attributes a transaction to a collaborator | ### Feature-dependent triggers | ID | Label | Requires | |---|---|---| | `boundCouponUsed` | Coupon Code Used | `Features::Coupons` | | `boundPostUsed` | Blog Post Visited | `Features::Posts` | | `collaboratorProductSold` | Collaborator Product Sold | (product ownership) | | `courseCompleted` | Course Completed | `Features::Courses` | | `lessonCompleted` | Lesson Completed | `Features::Lessons` | | `collaboratorFormSubmitted` | Form Submitted | `Features::Forms` | Extensions can register custom engagement triggers via the `EngagementTriggerRegistryInitiated` event. ## Conversion types | ID | Label | Supported incentive types | |---|---|---| | `sale` | Sale | `saleFixedPerTransaction`, `saleFixedPerProduct`, `saleTransactionPercentage` | | `lead` | Lead | `leadFixed` | | `renewal` | Renewal | (requires `Features::Renewals`) | ## Incentive types Incentive types define how reward amounts are calculated. ### Core (Lite + Essentials) | ID | Label | Description | |---|---|---| | `saleFixedPerTransaction` | Fixed per transaction | Flat amount per qualifying transaction | | `saleTransactionPercentage` | Percentage of transaction | Percentage of the transaction total | ### Essentials only | ID | Label | Description | |---|---|---| | `saleFixedPerProduct` | Fixed per product | Flat amount per qualifying product in a transaction | | `leadFixed` | Fixed per lead | Flat amount per lead conversion | ## Incentive resolvers Resolvers determine how rewards are distributed when multiple collaborators have engagements for the same conversion. ### Core (Lite + Essentials) | ID | Label | Description | |---|---|---| | `newestBindingWins` | Newest engagement wins | Last collaborator to engage gets the full reward | | `oldestBindingWins` | Oldest engagement wins | First collaborator to engage gets the full reward | ### Essentials only | ID | Label | Description | |---|---|---| | `topScoreWins` | Top score wins | Highest engagement score gets the full reward | | `evenlySharedPool` | Shared engagement pool | Reward split equally among all engaged collaborators | | `everyBindingWins` | Every engagement wins | Each engaged collaborator receives the full amount | | `performanceSharePool` | Performance-weighted pool | Reward distributed proportionally by engagement score | ## Program group sorters When programs are in a group, the sorter determines which program takes priority for a given conversion. | ID | Description | |---|---| | `newestBindingWins` | Most recent engagement determines which program fires | | `oldestBindingWins` | First engagement determines which program fires | ## Collaborator group structures A collaborator group's `structure` is the id of a registered structure resolver. The resolver determines how members relate to one another and which [walker capabilities](#walker-capabilities) the group provides. See [Choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) for the picker guide. | ID | Tier | Description | |---|---|---| | `flat` | Plus | No hierarchy. Every member is a peer. Provides no walker capabilities. | | `linearChain` | Pro | Members are ordered by a `position` value. Provides `hasLayer`. | | `parentChild` | Pro | Members form a tree by `parentCollaboratorId`. Provides `hasLayer`. | The installed resolvers and the capabilities each one advertises are also available at runtime via `GET /collaborator-groups/structures` (see [Collaborator Groups](/documentation/resource-reference/collaborator-groups)). ## Calculation strategies A `calculationType` is the id of a registered calculation strategy. It is stored on both sides of a calculation: on program engagement types and on distributor metric types. The strategy determines how a reward is spread across a group's members. See [Choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy) for the picker guide. | ID | Tier | Requires | Description | |---|---|---|---| | `fixed` | Core | (none) | Credits the converting collaborator only, with no layered distribution. Valid against any structure, including `flat`. | | `upline` | Pro | `hasLayer` | Credits collaborators above the converting member, walking the group upward by layer. | | `downline` | Pro | `hasLayer` | Credits collaborators below the converting member, walking the group downward by layer. | The **Requires** column is the [walker capability](#walker-capabilities) the strategy needs from the bound group's structure. The calc picker hides `upline` and `downline` when the bound structure does not provide `hasLayer`, which is why they do not appear for a `flat` group. `fixed` requires nothing, so it is always available. ## Walker capabilities The `WalkerCapability` enum identifies what a structure's walkers can do. A calculation strategy can require a capability, and the calc picker hides strategies whose required capabilities the bound group's structure does not provide. | Constant | Value | Description | |---|---|---| | `WalkerCapability::HAS_LAYER` | `hasLayer` | The structure's walkers yield layered steps, so a calc can credit collaborators per layer. Provided by `linearChain` and `parentChild`, not by `flat`. Required by the `upline` and `downline` calculation strategies. | **Source:** `Siren\Pro\Core\Groups\Structure\Enums\WalkerCapability` ## Line item filter types Programs can filter which line items in a transaction are eligible for reward calculation. | Type | Description | |---|---| | `inCategories` | Only items in specific product categories | | `withSkus` | Only items matching specific SKUs | | `withTypes` | Only items of a specific type (`products` or `subscriptions`) | | `collaboratorOwned` | Only items owned by the collaborator | ## Distribution frequency Distributors use these frequencies to schedule reward distributions. | Value | Description | |---|---| | `weekly` | Distributes rewards every week | | `monthly` | Distributes rewards every month | | `yearly` | Distributes rewards every year | ## Roles | Constant | Value | Description | |---|---|---| | `Roles::Collaborator` | `collaborator` | Affiliate or partner participating in programs | | `Roles::PlatformManager` | `platform_manager` | Full administrative access | | `Roles::FulfillmentManager` | `fulfillment_manager` | Can manage payouts and fulfillments | | `Roles::ProgramManager` | `program_manager` | Can manage programs and collaborators | | `Roles::CreativeManager` | `creative_manager` | Can manage promotional creatives | | `Roles::GoalManager` | `goal_manager` | Can manage goals and targets | The **Value** column is the base identifier from the `Roles` enum. The role slug actually registered in WordPress and checked by the capability gates is prefixed with `siren_`. For example, `Roles::PlatformManager` is registered as the role `siren_platform_manager`, and `Roles::Collaborator` as `siren_collaborator`. The built-in WordPress `administrator` role is the exception and is used unprefixed. When you call `current_user_can()` or `add_role()`, use the `siren_`-prefixed slug rather than the bare enum value. ### Managed resources Capabilities are generated per resource by pairing an action with a model class. `CollaboratorGroup` (`Siren\Plus\Core\Groups\Models\CollaboratorGroup`) is a permissioned resource: every collaborator-group endpoint requires the matching capability on it. Only `administrator` and `siren_platform_manager` receive full create, read, update, and delete capabilities on `CollaboratorGroup`. See [Collaborator Groups](/documentation/resource-reference/collaborator-groups) for the endpoints these capabilities gate. ## Error Handling Source: https://www.sirenaffiliates.com/documentation/extensions/wp-error-handling How WP_Error and is_wp_error() translate to Siren's typed exception system. import CodeTabs from "@/components/content/CodeTabs.astro"; # Error Handling WordPress uses a dual-path error model: some functions return `WP_Error` objects on failure (which you check with `is_wp_error()`), while others return `false` or `null`. This means every call site needs defensive checks, and the type of error you get is inconsistent across the API. Siren uses PHP exceptions. When something fails, it throws a typed exception. You catch what you need and let the rest propagate. ```php // WordPress: check for WP_Error after every operation $result = wp_insert_post($args); if (is_wp_error($result)) { echo $result->get_error_message(); return; } // Some functions return false instead of WP_Error $meta = get_post_meta($post_id, '_key', true); if ($meta === false) { // Was it an error, or does the meta just not exist? } ``` ```php // Siren: catch typed exceptions use PHPNomad\Datastore\Exceptions\RecordNotFoundException; use PHPNomad\Datastore\Exceptions\DuplicateEntryException; try { $program = $this->programs->getById($id); } catch (RecordNotFoundException $e) { // The program does not exist — handle it } try { $this->collaborators->create($data); } catch (DuplicateEntryException $e) { // A collaborator with this email already exists } ``` ## Exception reference Siren and PHPNomad use typed exceptions that tell you exactly what went wrong. Here are the most common ones you will encounter in extension development: | Exception | When thrown | |---|---| | `RecordNotFoundException` | `getById()` with a nonexistent ID, or a query that expects exactly one result finds none | | `DuplicateEntryException` | Creating a record that violates a uniqueness constraint (e.g., duplicate email) | | `RestException` | Generic REST error that carries an HTTP status code and message | | `ValidationException` | Request validation fails, includes field-level error details | | `AuthorizationException` | Authenticated but insufficient permissions (HTTP 403) | | `AuthenticationException` | Not authenticated at all (HTTP 401) | ## How does this work in REST controllers? Siren's REST controllers automatically catch these exceptions and return appropriate HTTP responses. You do not need to manually convert exceptions to REST error responses: - `RecordNotFoundException` returns a 404 response - `ValidationException` returns a 422 response with field-level errors - `AuthorizationException` returns a 403 response - `AuthenticationException` returns a 401 response - `RestException` returns whatever HTTP status code it carries This means your controller methods can focus on the happy path. Throw an exception when something goes wrong, and the framework handles the REST error formatting. ## What should I catch in extension code? In extension code (listeners, transformers, services), catch the exceptions you can meaningfully handle and let the rest propagate. Common patterns: ```php // In a transformer: catch RecordNotFoundException when looking up mappings try { $transaction = $this->transactions->getById($mappedId); } catch (RecordNotFoundException $e) { // No Siren transaction for this order — return null to skip return null; } // In a listener: catch and log, but don't crash the event pipeline try { $this->externalService->sync($data); } catch (\Exception $e) { $this->logger->logException($e); // The sync failed, but the rest of the pipeline should continue } ``` The transformer pattern is especially important. Transformers return `null` to skip events, so catching "record not found" and returning `null` is the standard way to handle cases where the data does not exist. If you do not catch an exception in a listener, it will propagate up and may prevent subsequent listeners for the same event from executing. When in doubt, catch broadly in listeners and log the exception so you can diagnose issues without breaking the event pipeline. ## Where to go next For how REST controllers auto-catch exceptions and convert them to HTTP responses, see [REST API Patterns](/documentation/extensions/wp-rest-api). For the full REST error response format (including validation error details), see the [Resource Reference Introduction](/documentation/resource-reference/introduction). For the full PHPNomad framework documentation, see [PHPNomad](https://phpnomad.com/). ## Event Bindings and Transformers Source: https://www.sirenaffiliates.com/documentation/extensions/event-bindings-and-transformers How getEventBindings() connects platform hooks to Siren domain events, and how transformer callbacks produce those events. # Event Bindings and Transformers Event bindings are the bridge between platform hooks and Siren's domain events. When a sale completes, a refund processes, or a coupon is applied, the platform fires a hook. Your extension's `getEventBindings()` method declares which hooks trigger which domain events. Each binding provides a callable that receives the hook's parameters and returns a domain event instance or `null`. Transformer service classes are the standard way to organize those callables. They encapsulate the logic for locating opportunities, preventing duplicates, and constructing events. But they are a convention for testability and separation of concerns, not a requirement. Anything callable works. If a closure gets the job done for a simple binding, use a closure. For lifecycle context on where `getEventBindings()` fits in the extension bootstrap process, see [The Integration Class](/documentation/extensions/integration-class). For the domain events themselves, see [Commerce Events](/documentation/extensions/commerce-events). ## What format do event bindings use? `getEventBindings()` returns an associative array keyed by domain event class name. Each value is an array of binding definitions, where each binding maps a platform hook to a callable: ```php public function getEventBindings(): array { return [ SaleTriggered::class => [ ['action' => 'platform_order_created', 'transformer' => $callable], ['action' => 'platform_order_completed', 'transformer' => $callable], ], RefundTriggered::class => [ ['action' => 'platform_order_refunded', 'transformer' => $callable], ], ]; } ``` Each binding definition has two keys: | Key | Type | Description | |-----|------|-------------| | `action` | `string` | The platform hook name to listen on (e.g., `woocommerce_new_order`, `edd_insert_payment`) | | `transformer` | `callable` | A callable that receives the hook's parameters and returns a domain event or `null` | When the platform fires the hook, the framework calls your transformer and, if it returns a non-null event, broadcasts that event to Siren's domain layer. The framework handles all the dispatching. Your extension only produces the event. For the full PHPNomad specification this builds on, see [Event Binding](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding) in the PHPNomad docs. ## How should the transformer callable work? The callable receives whatever parameters the platform hook passes and returns a domain event instance or `null`. The parameter signature must match the hook's invocation exactly. Always resolve services from the DI container rather than instantiating them directly. This ensures proper dependency injection and keeps your callables testable: ```php // Correct: resolve from the container fn($orderId) => $this->container->get(SaleTriggeredTransformer::class) ->getSaleTriggeredEvent($orderId) // Wrong: direct instantiation bypasses DI fn($orderId) => (new SaleTriggeredTransformer()) ->getSaleTriggeredEvent($orderId) ``` Different platforms pass different parameters to their hooks, so the callable's signature varies by integration. The transformer adapts those platform-specific parameters into a standardized domain event: ```php // WooCommerce: hook passes order ID fn($orderId) => $this->container->get(TransactionTransformerService::class) ->getSaleTriggeredEvent($orderId) // EDD: hook passes order ID and order data array fn($orderId, $orderData) => $this->container->get(SaleTriggeredTransformer::class) ->getSaleTriggeredEvent($orderId, $orderData) // NorthCommerce: hook passes event name, table, old data, new data, processed flag fn($event, $table, $old, $new, $processed) => $this->container->get(SaleTriggeredTransformer::class) ->getSaleTriggeredEvent($event, $table, $old, $new, $processed) // Gravity Forms: hook passes entry array and form array fn($entry, $form) => $this->container->get(SaleTriggeredTransformer::class) ->getSaleTriggeredEvent($entry, $form) ``` Each callable delegates to a method on a service class, but the callable itself is just a thin wrapper that routes platform-specific parameters to the right service method. This separation keeps your `getEventBindings()` method clean and declarative. ## What happens when the callable returns null? A `null` return means "no event should fire." The framework silently skips it. This is not an error condition. It is the normal and expected outcome for most hook invocations. Common reasons a transformer returns `null`: - The customer was not referred by a collaborator, so there is no affiliate opportunity. - The mapping table shows this order has already been processed (duplicate order). - The order has no line items or the payment has not been confirmed. - The hook fired but the context does not apply (e.g., a NorthCommerce table change event that is not related to orders). Design your transformers to return `null` liberally. It is always safer to skip than to produce a malformed event. ## Can multiple hooks trigger the same event? Yes. A single domain event can be bound to multiple platform hooks. This is common when platforms expose different hooks for overlapping scenarios, or when you need to capture the same business outcome from multiple entry points. WooCommerce binds `SaleTriggered` to three hooks because a new order can arrive through different paths: ```php SaleTriggered::class => [ ['action' => 'woocommerce_new_order', 'transformer' => $saleCallback], ['action' => 'woocommerce_order_status_processing', 'transformer' => $saleCallback], ['action' => 'woocommerce_order_status_completed', 'transformer' => $saleCallback], ], ``` `RefundTriggered` is an even more dramatic example. WooCommerce has seven distinct hooks for the various ways an order can be reversed: ```php RefundTriggered::class => [ ['action' => 'woocommerce_order_status_completed_to_failed', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_completed_to_cancelled', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_completed_to_refunded', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_processing_to_failed', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_processing_to_cancelled', 'transformer' => $refundCallback], ['action' => 'woocommerce_order_status_processing_to_refunded', 'transformer' => $refundCallback], ['action' => 'wc-completed_to_trash', 'transformer' => $refundCallback], ], ``` When multiple hooks share a callback, duplicate prevention inside the transformer keeps things safe. The second hook to fire for the same order will find an existing mapping and return `null`. ## Can bindings be conditional? Yes. Because `getEventBindings()` returns a plain array, you can add or omit entries based on runtime conditions. This is useful when an event only makes sense if an optional dependency is installed. WooCommerce only adds `RenewalTriggered` bindings when WooCommerce Subscriptions is available: ```php public function getEventBindings(): array { $triggers = [ SaleTriggered::class => [ ['action' => 'woocommerce_new_order', 'transformer' => $saleCallback], ], // ... other bindings ]; if (class_exists(WC_Subscription::class)) { $triggers[RenewalTriggered::class] = [ ['action' => 'woocommerce_subscription_renewal_payment_complete', 'transformer' => $renewalTransformerCallback], ]; } return $triggers; } ``` If the class does not exist, the `RenewalTriggered` binding is simply absent from the returned array and no renewal hooks are registered. ## What is the transformer service pattern? Any callable works as a transformer. A closure, a static method reference, an invokable class. But for real integrations, you want testability, dependency injection access, and a clear separation between hook-routing logic and event-construction logic. The convention is a dedicated transformer service class that the container resolves. A transformer service has three responsibilities: 1. Locate the affiliate opportunity. Determine which collaborator (if any) referred this customer. 2. Check for duplicates. Query the mapping table to ensure this platform entity has not already been processed. 3. Construct the domain event. Build a transaction details array (via an adapter) and return the event instance. Here is an annotated walkthrough of a sale transformer: ```php public function getSaleTriggeredEvent($orderId): ?SaleTriggered { // Normalize input — some hooks pass an order object, others pass an ID $orderId = WC()->order_factory->get_order_id($orderId); if (!$orderId) return null; // 1. Locate the affiliate opportunity $opportunity = $this->locateOpportunity($orderId); if (!$opportunity) return null; // 2. Check for duplicates via mapping table if ($this->orderHasTransaction($orderId)) return null; // 3. Build transaction details via adapter $transactionDetails = $this->detailsAdapter->toArray($orderId); if (empty($transactionDetails)) return null; // 4. Construct and return the event return new SaleTriggered( $opportunity->getId(), $transactionDetails, 'wc', // source extension ID $orderId, // external binding ID 'wc_order' // external binding type ); } ``` The opportunity location method uses `OpportunityLocatorService` with a `VisitorOpportunity` strategy. This strategy resolves the affiliate opportunity from the visitor's tracking data (cookie, referral URL, etc.): ```php protected function locateOpportunity($orderId): ?Opportunity { try { return $this->opportunityLocator->locate( new VisitorOpportunity($this->visitorIdResolver->resolve()) ); } catch (RecordNotFoundException $e) { return null; } } ``` The duplicate check queries the `MappingDatastore` to see if this external order ID has already been mapped to a Siren transaction: ```php protected function orderHasTransaction($orderId): bool { try { $this->mappings->getByExternalId($orderId, 'wc_order', 'transaction'); return true; } catch (RecordNotFoundException $e) { return false; } } ``` If the mapping exists, the order has already been tracked and the transformer returns `null`. This is what makes multi-hook bindings safe. ## How do transformer variants differ by event type? Not all transformers follow the full three-step pattern. The shape depends on the event type and what information is available at the time the hook fires. For constructor signatures and event payloads, see [Commerce Events](/documentation/extensions/commerce-events). For downstream listener behavior, see the [Events Reference](/documentation/developer-reference/events-introduction). Sale transformers use the full three-step pattern: locate opportunity, check for duplicates, construct event with transaction details from the adapter. This is the most involved transformer because it creates a new transaction from scratch. See [SaleTriggered](/documentation/developer-reference/events-commerce/sale-triggered). Approval transformers (`TransactionCompleted`) are simpler. The transaction already exists from a previous `SaleTriggered` event, so the transformer just looks it up by mapping and wraps it in the event. No opportunity location or adapter is needed: ```php public function getTransactionCompletedEvent($orderId): ?TransactionCompleted { try { $mapping = $this->mappings->getByExternalId($orderId, 'wc_order', 'transaction'); $transaction = $this->transactions->find($mapping->getLocalId()); return new TransactionCompleted($transaction); } catch (RecordNotFoundException $e) { return null; } } ``` If no mapping exists, the order was never tracked by Siren, so there is nothing to approve. See [TransactionCompleted](/documentation/developer-reference/events-commerce/transaction-completed). Refund transformers follow the same pattern as approval transformers. Look up the existing transaction by mapping, wrap it in the event. If no mapping exists, return `null`. See [RefundTriggered](/documentation/developer-reference/events-commerce/refund-triggered). Coupon transformers use `CurrentUserOpportunity` locators instead of `VisitorOpportunity` because the coupon is applied by the logged-in customer during checkout, not resolved from visitor tracking. Duplicate prevention is not needed because coupon application events are idempotent. See [CouponApplied](/documentation/developer-reference/events-commerce/coupon-applied). Renewal transformers must trace the renewal order back to the original subscription's initial transaction. They query the mapping table to find the original transaction, then perform a duplicate check on the renewal order ID to avoid double-counting. See [RenewalTriggered](/documentation/developer-reference/events-commerce/renewal-triggered). ## What external type conventions should I follow? When constructing events that reference external platform entities (like `SaleTriggered` or `RenewalTriggered`), you provide an external type string that identifies the kind of entity. The convention is `{extension_id}_{entity}`: | Extension | External Type | Description | |-----------|---------------|-------------| | WooCommerce (`wc`) | `wc_order` | A WooCommerce order | | WooCommerce (`wc`) | `wc_product` | A WooCommerce product | | EDD (`edd`) | `edd_order` | An EDD payment/order | | NorthCommerce (`nc`) | `nc_order` | A NorthCommerce order | | LifterLMS (`llms`) | `llms_order` | A LifterLMS order | The extension ID prefix matches the value returned by your `Integration::getId()` method. This keeps external types globally unique across integrations and allows the mapping system to route lookups to the correct extension. For the full mapping system, including how these external types are stored and queried, see [The Mapping System](/documentation/extensions/mappings). ## What dependencies do transformer services typically need? Transformer services are resolved from the DI container, so their constructor dependencies are automatically injected. Here are the common ones: | Dependency | Purpose | |------------|---------| | `OpportunityLocatorService` | Finds the affiliate opportunity for a visitor or user | | `VisitorOpportunity` or `CurrentUserOpportunity` | Strategy objects that tell the locator how to resolve the opportunity | | `MappingDatastore` | Duplicate prevention and external-to-internal ID lookups | | `LoggerStrategy` | Logs exceptions from datastore operations | | `OrderToTransactionDetailsAdapter` | Converts platform order data to Siren's standardized detail format | | `TransactionDatastore` | Fetches existing transactions for approval, refund, and renewal transformers | Sale transformers use most of these. Approval and refund transformers typically only need `MappingDatastore`, `TransactionDatastore`, and `LoggerStrategy`. For the adapter's output format, see [Adapters](/documentation/extensions/adapters). ## Where should transformer files live? The convention varies slightly between extensions, but the pattern is consistent: transformer classes live in their own subdirectory within the extension, one class per event type. - EDD, NorthCommerce, and Gravity Forms use a `Transformers/` subdirectory (e.g., `SaleTriggeredTransformer.php`, `RefundTriggeredTransformer.php`) - WooCommerce and LifterLMS use a `Services/` subdirectory (e.g., `TransactionTransformerService.php`, `RefundTransformerService.php`) Either convention works. The important thing is that each transformer is a standalone class with a clear single responsibility: one event type per class. ## Complete example: EDD getEventBindings() Here is the full `getEventBindings()` implementation from the EDD integration, showing all five event types wired up in a single method. This is a good reference because it covers every commerce event type cleanly: ```php public function getEventBindings(): array { $saleTransformerCallback = fn($orderId, $orderData) => $this->container->get( SaleTriggeredTransformer::class )->getSaleTriggeredEvent($orderId, $orderData); $transactionCompletedTransformer = fn($orderId, $payment, $customer) => $this->container->get( TransactionCompletedTransformer::class )->getTransactionCompletedEvent($orderId, $payment, $customer); $renewalTransformerCallback = fn($subscriptionId, $expiration, $subscription, $paymentId) => $this->container->get( RenewalTriggeredTransformer::class )->getRenewalTriggeredEvent($subscriptionId, $expiration, $subscription, $paymentId); $couponTransformerCallback = fn($couponCode, $discounts) => $this->container->get( CouponAppliedTransformer::class )->getCouponAppliedEvent($couponCode, $discounts); $refundTransformerCallback = fn($payment) => $this->container->get( RefundTriggeredTransformer::class )->getRefundTriggeredEvent($payment); return [ SaleTriggered::class => [ ['action' => 'edd_insert_payment', 'transformer' => $saleTransformerCallback], ['action' => 'edd_post_add_manual_order', 'transformer' => $saleTransformerCallback], ], CouponApplied::class => [ ['action' => 'edd_cart_discount_set', 'transformer' => $couponTransformerCallback], ], TransactionCompleted::class => [ ['action' => 'edd_complete_purchase', 'transformer' => $transactionCompletedTransformer], ], RefundTriggered::class => [ ['action' => 'edd_post_refund_payment', 'transformer' => $refundTransformerCallback], ], RenewalTriggered::class => [ ['action' => 'edd_subscription_post_renew', 'transformer' => $renewalTransformerCallback], ], ]; } ``` Several things to notice: - Each transformer callback is defined as a variable before the return statement. This keeps the return array readable. - Each callback resolves its service from the container lazily. The service is not instantiated until the hook actually fires. - The parameter signatures match EDD's hook invocations exactly. `edd_insert_payment` passes `($orderId, $orderData)`, `edd_complete_purchase` passes `($orderId, $payment, $customer)`, and so on. - `SaleTriggered` is bound to two hooks: `edd_insert_payment` for standard purchases and `edd_post_add_manual_order` for admin-created orders. The duplicate check inside the transformer prevents double-counting. - All five event types are represented: sale creation, payment approval, refund, coupon application, and subscription renewal. This pattern scales to any platform. Replace the hook names with your platform's equivalents, adjust the parameter signatures, and implement the corresponding transformer service classes. ## Event Execution Order Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-execution-order Scannable reference of every Siren event in execution order, organized by flow type. The debugging companion to the narrative pipeline overview. import EventFlow from "@/components/content/EventFlow.astro"; # Event Execution Order This page lists every Siren event in the order it fires, organized by flow type. It is a flat lookup table you can keep open in a tab while debugging the pipeline. For narrative explanations of why each event exists and how the stages connect, see [The Attribution Pipeline](/documentation/resource-reference/pipeline-overview) overview. For full payload and listener documentation, follow the links on each event name to its detail page. ## Standard Sale Flow The main pipeline from referral click to payout. 1. [OpportunityTriggered](/documentation/developer-reference/events-attribution/opportunity-triggered). Customer visits via referral link. Opportunity record created. 2. [EngagementsTriggered](/documentation/developer-reference/events-attribution/engagements-triggered). Engagement trigger strategies create credit claims per program. 3. [SaleTriggered](/documentation/developer-reference/events-commerce/sale-triggered). Extension detects a purchase and produces the commerce event. 4. [TransactionCreateRequested](/documentation/developer-reference/events-payments/transaction-create-requested). Mutable event. Listeners can modify transaction details before persistence. 5. [TransactionCreated](/documentation/developer-reference/events-payments/transaction-created). Transaction record persisted to database. 6. [ConversionInitialized](/documentation/developer-reference/events-conversions/conversion-initialized). Conversion pipeline begins processing. 7. [ProgramGroupConversionTriggered](/documentation/developer-reference/events-conversions/program-group-conversion-triggered). *(If program groups apply)* Winning program identified among mutually exclusive group. 8. [ConversionsAwarded](/documentation/developer-reference/events-conversions/conversions-awarded). Credit assigned per qualifying program. 9. [EngagementAwarded](/documentation/developer-reference/events-attribution/engagement-awarded). Individual engagement awarded credit during conversion. 10. [EngagementCompleted](/documentation/developer-reference/events-attribution/engagement-completed). All engagements for the opportunity processed. 11. [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued). Business records what it owes the collaborator (draft status). 12. [TransactionCompleted](/documentation/developer-reference/events-commerce/transaction-completed). Payment gateway confirms the charge. *May fire immediately or after a delay depending on the gateway.* 13. [ConversionApproved](/documentation/developer-reference/events-conversions/conversion-approved). Conversion passes review (auto on payment confirmation, or manual by admin). > Steps 12-13 can be asynchronous. TransactionCompleted fires when the payment gateway confirms, which may be immediate or delayed. ConversionApproved can also be triggered manually by an admin. ## Fulfillment & Payout Flow Shared by all flows. Triggered when an admin initiates a fulfillment run, not immediately after conversion approval. 1. [FulfillmentCreated](/documentation/developer-reference/events-payments/fulfillment-created). Payout batch initiated. 2. [FulfillmentStatusChanged](/documentation/developer-reference/events-payments/fulfillment-status-changed). Fulfillment transitions through lifecycle (pending, processing, completed). 3. [PayoutCreated](/documentation/developer-reference/events-payments/payout-created). Individual disbursement line item built per collaborator. 4. [PayoutPaid](/documentation/developer-reference/events-payments/payout-paid). Collaborator receives payment (terminal event). 5. [ObligationCompleted](/documentation/developer-reference/events-payments/obligation-completed). Obligation marked fulfilled. Payout ID written back. ## Coupon Attribution Flow Diverges from standard sale at the beginning. 1. [CouponApplied](/documentation/developer-reference/events-commerce/coupon-applied). Customer uses coupon at checkout. System looks up coupon owner. 2. [EngagementsTriggered](/documentation/developer-reference/events-attribution/engagements-triggered). Engagements created from coupon ownership. 3. [SaleTriggered](/documentation/developer-reference/events-commerce/sale-triggered). Sale completes with coupon attribution. 4. *Continues from step 4 of Standard Sale Flow (TransactionCreateRequested onward).* ## Lead Flow Uses LeadTriggered instead of SaleTriggered. No transaction details, no TransactionCompleted. 1. [OpportunityTriggered](/documentation/developer-reference/events-attribution/opportunity-triggered). Customer visits via referral link. Opportunity record created. 2. [EngagementsTriggered](/documentation/developer-reference/events-attribution/engagements-triggered). Engagement trigger strategies create credit claims per program. 3. [LeadTriggered](/documentation/developer-reference/events-commerce/lead-triggered). Form submission or signup action detected (no monetary value). 4. [ConversionInitialized](/documentation/developer-reference/events-conversions/conversion-initialized). Conversion pipeline begins processing. 5. [ConversionsAwarded](/documentation/developer-reference/events-conversions/conversions-awarded). Credit assigned using lead-specific incentive types. 6. [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued). Business records what it owes the collaborator (draft status). 7. [ConversionApproved](/documentation/developer-reference/events-conversions/conversion-approved). Manual admin approval required (no auto-approval since there is no TransactionCompleted). ## Renewal Flow Starts from existing engagement records. Uses ConversionRenewed instead of ConversionInitialized. 1. [RenewalTriggered](/documentation/developer-reference/events-commerce/renewal-triggered). Subscription renewal payment detected. Traces back to original transaction. 2. [ConversionRenewed](/documentation/developer-reference/events-conversions/conversion-renewed). Conversion pipeline begins from original engagement records. 3. [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued). Business records what it owes the collaborator (draft status). 4. [TransactionCompleted](/documentation/developer-reference/events-commerce/transaction-completed). Payment gateway confirms the charge. 5. [ConversionApproved](/documentation/developer-reference/events-conversions/conversion-approved). Conversion passes review (auto on payment confirmation). ## Refund Flow Short reversal flow. 1. [RefundTriggered](/documentation/developer-reference/events-commerce/refund-triggered). Completed order reversed (cancellation, refund, or deletion). 2. [ConversionRejected](/documentation/developer-reference/events-conversions/conversion-rejected). Conversions tied to the refunded transaction are denied. Obligations cleaned up. ## Distribution Flow Separate pipeline for scheduled, metric-based rewards. 1. [MetricsTriggered](/documentation/developer-reference/events-distributions/metrics-triggered). New metric data recorded (fires continuously as activity happens, between distribution periods). 2. [DistributionHeartbeatInitialized](/documentation/developer-reference/events-distributions/distribution-heartbeat-initialized). Scheduled trigger fires. 3. [DistributionCompleted](/documentation/developer-reference/events-distributions/distribution-completed). Distribution period processed. Reward pool calculated. 4. [AllocationCompleted](/documentation/developer-reference/events-distributions/allocation-completed). Individual collaborator's share determined. 5. [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued). Allocation becomes an obligation (enters fulfillment pipeline). > MetricsTriggered fires continuously as activity happens. Steps 2-5 fire on the heartbeat schedule. ## Manual Attribution Bypasses normal engagement flow entirely. 1. [ManualAttributionRequested](/documentation/developer-reference/events-conversions/manual-attribution-requested). Admin manually attributes conversion to a collaborator. 2. [ConversionsAwarded](/documentation/developer-reference/events-conversions/conversions-awarded). Credit assigned per qualifying program. 3. [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued). Business records what it owes the collaborator (draft status). ## System Events Non-pipeline events that fire independently. - [CollaboratorSubmissionReceived](/documentation/developer-reference/events-system/collaborator-submission-received). New collaborator registration request (mutable). - [CollaboratorAccountReady](/documentation/developer-reference/events-system/collaborator-account-ready). Collaborator account fully set up. - [StudentCompletedLesson](/documentation/developer-reference/events-system/student-completed-lesson). Student finishes a lesson in a connected LMS. - [RecipeApplyRequested](/documentation/developer-reference/events-system/recipe-apply-requested). Recipe configuration applied (mutable). - [OpportunityInvalidated](/documentation/developer-reference/events-attribution/opportunity-invalidated). Opportunity rejected (duplicate, expired, validation failure). Pipeline terminates. ## Collaborator Group Events Non-pipeline lifecycle events for collaborator groups and their bindings. These fire on collaborator group REST operations rather than in the attribution or distribution flows. For full payload and listener documentation, follow each event link to its detail page. - [CollaboratorGroupCreated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-created). A new collaborator group is created via the REST API. - [CollaboratorGroupRenamed](/documentation/developer-reference/events-collaborator-groups/collaborator-group-renamed). A group's name or description is updated. - [CollaboratorGroupStructureChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-changed). A group's structure resolver id is changed. Per-member metadata is not migrated when the structure changes. - [CollaboratorGroupDeleted](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted). A group is deleted. - [CollaboratorAddedToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-added-to-collaborator-group). A collaborator is added as a member of a group. - [CollaboratorRemovedFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/collaborator-removed-from-collaborator-group). A collaborator is removed from a group. - [CollaboratorGroupMemberMetadataChanged](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-metadata-changed). A member's structural metadata (such as `position` or parent) is changed on an established group. - [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group). A program is bound to a group by passing a `collaboratorGroupId` on the program create or update request. - [ProgramUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group). A program's group binding is cleared. - [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group). A distributor is bound to a group by passing a `collaboratorGroupId` on the distributor create or update request. - [DistributorUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group). A distributor's group binding is cleared. Three registry-initiated events broadcast once when their registries are first read, so extensions can register their own resolvers. [CollaboratorGroupStructureResolverRegistryInitiated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-structure-resolver-registry-initiated) is the seam Pro tiers use to add hierarchical structures. [CollaboratorGroupResolverRegistryInitiated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-resolver-registry-initiated) and [CollaboratorGroupMemberResolverRegistryInitiated](/documentation/developer-reference/events-collaborator-groups/collaborator-group-member-resolver-registry-initiated) cover the group and member resolver registries. ## Events Source: https://www.sirenaffiliates.com/documentation/resource-reference/events Event ingestion API. POST a registered slug to create a domain event from outside the WordPress runtime — site visits, sales, refunds, and anything else extensions register. # Events The event ingestion API turns external HTTP requests into Siren domain events. A POST to `/event/{slug}` looks up a registered factory by slug, builds the corresponding event from the request body, and broadcasts it through the same event system that powers WordPress-rendered traffic. Engagement triggers, conversion building, and obligation creation all run identically — the API is just a transport. This is the primary interface a headless frontend, a connector plugin, or any non-WordPress runtime uses to participate in Siren's attribution pipeline. The [headless attribution guide](/documentation/headless/attribution) walks through the most common usage end to end. ## Endpoints | Method | Path | Description | |---|---|---| | POST | `/event/site-visited` | [Report a site visit](/documentation/resource-reference/events/site-visited). Creates or updates an opportunity for a referred visitor. | | POST | `/event/sale` | [Report a sale](/documentation/resource-reference/events/sale). Creates a sale event from a non-WordPress checkout — typically used by the Siren Connect plugin or a custom commerce bridge. | | POST | `/event/refund` | [Report a refund](/documentation/resource-reference/events/refund). Reverses a previously reported sale by external ID. | ## Tier availability The event ingestion controller lives in `lib/Events`, which ships with Essentials and above. Lite does not include it. ## Authentication In a WordPress install, the endpoint binds a no-op authentication middleware and defers to the WordPress REST permission chain. Out of the box that means the endpoint accepts unauthenticated POSTs — sufficient for site-visit tracking, where the form of the request itself encodes everything that matters. If your deployment needs stricter access control on a particular slug, wrap the endpoint with the same WordPress REST authentication mechanisms you'd use on any custom REST route (a filter on `rest_pre_dispatch`, a CORS plugin's allowlist, an authenticated reverse proxy, etc.). In a SaaS deployment the same controller binds an API-key middleware instead and requires a Bearer token with the appropriate scope. ## Response shape Every event-ingestion endpoint returns `200 OK` with an empty body on success. The opportunity ID associated with the request — whether newly created or resolved from an inbound `X-Siren-OID` header or browser cookie — is written to the `X-Siren-OID` response header. The response also includes `Access-Control-Expose-Headers: X-Siren-OID` so JavaScript can read the value across origins. Validation failures return `400` with the field errors. Unknown slugs return `404`. ## Adding new event factories The factory registry is event-driven. An extension can listen for `EventFactoryRegistryInitiated` and call `addFactoryClass(MyEventFactory::class)` to register a new slug. The factory implements `EventFactory`, declares its slug, validation rules, and how to build a domain event from the request. The [extension quickstart](/documentation/extensions/quickstart) and [event bindings and transformers](/documentation/extensions/event-bindings-and-transformers) cover the patterns. ## Events Introduction Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-introduction How domain events drive Siren's pipeline. Covers listening to events, event flow, and when to use events vs direct data access. import CodeTabs from "@/components/content/CodeTabs.astro"; import EventFlow from "@/components/content/EventFlow.astro"; # Events Introduction Siren is an event-driven system. Almost everything that happens in the attribution pipeline is a reaction to a domain event. When a customer clicks an affiliate link, an event fires. When a sale completes, an event fires. When conversions are awarded, obligations are created, or payouts are generated, each step is triggered by the event that came before it. The core business logic lives in event listeners, not in procedural service calls. This architecture is built on [PHPNomad's event system](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Every event implements the `Event` interface, every listener implements `CanHandle`, and the framework's initializer system wires them together through the DI container. Understanding events is essential for working with Siren at the code level, whether you are building an extension, debugging the pipeline, or writing custom integration logic. ## How do I listen to events? There are three ways to attach a listener to an event, ranging from the simplest to the most structured. ```php use PHPNomad\Core\Facades\Event; use Siren\Commerce\Events\SaleTriggered; // Closest to WordPress add_action — works anywhere, no setup required Event::attach(SaleTriggered::class, function(SaleTriggered $event) { $opportunityId = $event->getOpportunityId(); $details = $event->getTransactionDetails(); // React to the sale }); // You can also broadcast events from anywhere Event::broadcast(new SaleTriggered($opportunityId, $details, $source, $dataType, $bindingId)); ``` ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use PHPNomad\Events\Interfaces\HasListeners; use Siren\Commerce\Events\SaleTriggered; // Step 1: Create a handler class that implements CanHandle class OnSaleTriggered implements CanHandle { protected ObligationDatastore $obligations; // Constructor dependencies are injected by the DI container public function __construct(ObligationDatastore $obligations) { $this->obligations = $obligations; } public function handle(Event $event): void { $opportunityId = $event->getOpportunityId(); // Use injected services to react to the sale } } // Step 2: Register the handler in your extension's initializer class MyInitializer implements HasListeners { public function getListeners(): array { return [ // Maps event class => handler class (or array of handler classes) SaleTriggered::class => OnSaleTriggered::class, ]; } } ``` ```php use PHPNomad\Events\Interfaces\EventStrategy; use Siren\Commerce\Events\SaleTriggered; // Inject the event strategy into your service class MyService { protected EventStrategy $events; public function __construct(EventStrategy $events) { $this->events = $events; } public function setup(): void { $this->events->attach(SaleTriggered::class, function($event) { // React to the sale }); } } ``` The **Event facade** is the simplest option and works anywhere without setup, similar to WordPress's `add_action`. Use it in theme files, standalone scripts, or quick integrations. It also provides `Event::broadcast()` for firing events and `Event::detach()` for removing listeners. An optional priority parameter controls execution order. The **HasListeners** approach is preferred for extension code. Handler classes implement `CanHandle` and are resolved through the DI container at dispatch time, which means their constructors can inject any registered service. The initializer maps event classes to handler classes (or arrays of handler classes for multiple handlers on the same event). This keeps event subscriptions explicit and testable. The **EventStrategy** approach is available when you need to attach listeners from within a DI-wired service class, but don't want to declare them statically in an initializer. See the [Listeners & Event Handlers](/documentation/extensions/listeners) guide for the full details on writing handler classes. For a comparison of all approaches in the context of WordPress development, see the [WordPress Developer Guide](/documentation/extensions/wp-hooks-and-events). ## How does the event pipeline flow? Events in Siren form a pipeline that tracks the full lifecycle of a referral, from the initial customer interaction through to the collaborator payout. The pipeline begins with commerce events. When an e-commerce platform detects a sale, refund, or lead form submission, the appropriate extension produces a commerce event like `SaleTriggered` or `LeadTriggered`. These are the entry points into Siren's attribution system. Commerce events trigger attribution events. When a sale is triggered, the system locates the opportunity that tracked the customer's referral. It evaluates which collaborators had engagements with that opportunity, determines which programs apply, and fires events like `EngagementCompleted` and `TransactionTriggered` as it works through the attribution logic. Attribution events trigger conversion events. The conversion system picks up where attribution leaves off, building conversion records for each program that should credit a collaborator. The `ConversionsAwarded` event signals that credit has been assigned. Conversion events trigger payment events. When conversions are awarded, obligations are created to track what the business owes each collaborator. When conversions are approved (either automatically when payment confirms, or manually by an admin), obligations advance to pending status and become eligible for fulfillment and payout. This flow is not a rigid sequence of hardcoded steps. Each stage is connected only by events, which means extensions and custom code can hook into any point in the pipeline without modifying the core logic. ## When should I listen to events vs access data directly? Listening to events is for reacting to state changes as they happen. If you need to do something when a sale occurs, when a conversion is awarded, or when an obligation status changes, attach a listener to the relevant event. Your listener runs at the moment the state changes, with full context about what happened. Accessing datastores directly is for reading current state. If you need to display a list of active programs, look up a collaborator's engagement history, or generate a report from existing records, query the datastore. The data is already there; you do not need an event to access it. A common mistake is writing code that polls a datastore to detect changes, or that creates records manually instead of firing the appropriate event. If your logic starts with "check if something has changed since last time," you probably want a listener. If your logic starts with "show me the current state," you want a datastore query. ## What is documented in the following pages? The event reference is organized by pipeline stage. Each category page explains the flow and links to individual event pages with full details on payload, listeners, and code examples. - [Commerce Events](/documentation/developer-reference/events-commerce) covers the events produced by e-commerce integrations: sales, refunds, coupon usage, renewals, and leads. - [Attribution Events](/documentation/developer-reference/events-attribution) covers opportunity tracking, engagement creation, and the bridge from commerce activity into the conversion system. - [Conversion Events](/documentation/developer-reference/events-conversions) covers the conversion lifecycle from initialization through approval, rejection, and renewal. - [Payment Events](/documentation/developer-reference/events-payments) covers transaction creation, obligation issuance, fulfillment processing, and payouts. - [Distribution Events](/documentation/developer-reference/events-distributions) covers the scheduled reward system: heartbeat triggers, allocations, and metric tracking. - [System Events](/documentation/developer-reference/events-system) covers collaborator registration, LMS integration, and recipe import. - [Collaborator Group Events](/documentation/developer-reference/events-collaborator-groups) covers the group lifecycle and bindings: creation, renaming, structure changes, deletion, member changes, program and distributor binding, and structure resolver registration. ## Export Payouts Source: https://www.sirenaffiliates.com/documentation/resource-reference/payouts/export Exports selected payouts as CSV data and optionally marks them as paid. ### Export Payouts `POST /siren/v1/payouts/export` Exports selected payouts as CSV data and optionally marks them as paid. The CSV is returned in the JSON response body (not as a file download), allowing the client to trigger a download. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `ids` | integer[] | Yes | Array of payout IDs to export | | `markAsPaid` | boolean | No | Whether to mark exported payouts as paid (default: `true`) | #### CSV Columns The exported CSV includes Payout ID, Collaborator Email, Collaborator Name, Collaborator ID, Currency, and Value (converted to float from integer cents). If a collaborator record has been deleted, their email and name columns show ``. #### Example ```json { "ids": [20, 21, 22], "markAsPaid": true } ``` ```json { "success": true, "csv": "Payout ID,Collaborator Email,Collaborator Name,Collaborator ID,Currency,Value\n20,jane@example.com,Jane Doe,3,USD,30.00\n21,bob@example.com,Bob Smith,7,USD,20.00", "filename": "payouts_export_2026-04-06_120000.csv", "exported": 2, "marked": 2 } ``` Each payout marked as paid broadcasts a `PayoutPaid` event. ## Extension Architecture Overview Source: https://www.sirenaffiliates.com/documentation/extensions/architecture-overview How Siren extensions work — registration, lifecycle, event-driven architecture, and the PHPNomad interfaces that drive it all. import EventFlow from "@/components/content/EventFlow.astro"; # Extension Architecture Overview Siren's extension system is built on [PHPNomad](https://phpnomad.com/), a platform-agnostic PHP framework. If you're familiar with PHPNomad's [initializer system](https://phpnomad.com/core-concepts/bootstrapping/creating-and-managing-initializers/), you already understand how extensions work — Siren extensions are PHPNomad initializers with an additional `Extension` interface on top. If you're not familiar with PHPNomad, the links throughout this guide point to the relevant framework documentation. This article covers the parts that are specific to Siren: how extensions register, what the `Extension` interface adds, how Siren's domain events flow, and what types of extensions you can build. ## How do extensions register with Siren? A Siren extension is a standard WordPress plugin that registers itself through the `siren_ready` action. Each extension has its own `plugin.php`, its own `composer.json`, and its own namespace. Siren does not scan directories or rely on naming conventions — your extension tells Siren it exists by calling `Extensions::add()`. ```php // In your extension's plugin.php add_action('siren_ready', function () { Extensions::add(Integration::getId(), fn() => new Integration()); }, 0); ``` Registration happens at priority `0` and stores a lazy factory — your `Integration` class is not instantiated until Siren is ready to process it. On `plugins_loaded`, Siren retrieves all registered extensions and runs each one through PHPNomad's [initializer pipeline](https://phpnomad.com/core-concepts/bootstrapping/creating-and-managing-initializers/). That pipeline processes interfaces like [event bindings](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding), [listeners](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners), class definitions, and load conditions in a specific order — all documented on PHPNomad's site. Because extensions are independent plugins, they install and activate through the normal WordPress plugin admin, maintain their own version lifecycle, and only load when their dependencies are satisfied. ## What does the Extension interface add? Every Siren extension implements `Siren\Extensions\Core\Interfaces\Extension`, which builds on PHPNomad's initializer interfaces with Siren-specific metadata: ```php interface Extension extends Module { public function getName(): string; public function getDescription(): string; public function canActivate(): bool; public function getIsActive(): bool; public function getSupports(): array; // Features::* constants } ``` `getName()` and `getDescription()` provide human-readable metadata for Siren's admin UI. `canActivate()` reports whether the extension's dependencies exist — a WooCommerce extension returns `class_exists('WooCommerce')`, and Siren's admin uses this to show available vs. unavailable extensions. `getIsActive()` reports whether the extension has successfully loaded. `getSupports()` declares what capabilities the extension provides (coupons, renewals, forms, etc.) using `Features::*` constants, which Siren uses to conditionally enable UI features and engagement triggers. The `Module` parent interface adds `getId()` (a short unique string like `'wc'` or `'edd'` used for module registration and template paths) and `getRootPath()` (the filesystem path to the extension root). ## What types of extensions can I build? ### Third-party integrations These bridge an external system (WooCommerce, EDD, Gravity Forms, etc.) into Siren's domain model. Their primary job is translation: converting platform-specific hooks and data structures into Siren's domain events. See the [integration feature matrix](/documentation/general/integration-feature-matrix) for a comparison of what each built-in integration supports. A WooCommerce integration, for example, listens to `woocommerce_new_order` and transforms the WooCommerce order data into a `SaleTriggered` domain event that Siren's core can process without knowing anything about WooCommerce. Integrations typically implement [event bindings](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding) to map platform hooks to domain events, use transformers to convert hook arguments into event objects, use adapters to reshape platform data structures into Siren's formats, and gate loading behind a `canActivate()` check for the third-party plugin. ### Feature extensions These add new capabilities to Siren without necessarily integrating a third party. They typically implement [listeners](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners) to react to domain events and may register admin pages or other services. They usually don't need transformers or event bindings at all. The line between the two types is not rigid. Some extensions do both, integrating a third party while also adding Siren-specific features. ### Collaborator group, scoring, and eligibility seams The Plus-tier `CollaboratorGroup` primitive and the Pro-tier cascade scoring built on top of it are extensible through their own registry events. Each seam follows the same pattern as the integration and feature work above: you listen on a registry-initiated event and register your own class. Four seams are open here. Structure resolvers define how a group's members relate to each other (flat, linear chain, parent-child). You add a custom structure by registering a resolver. See [Collaborator Group Structures](/documentation/extensions/collaborator-group-structures). Walker capabilities describe what a structure's walker can do, such as the `hasLayer` capability that cascade calculations require. A structure declares the capabilities it provides, and a calculation declares the capabilities it needs. See [Walker Capabilities](/documentation/extensions/walker-capabilities). Calculation strategies turn an engagement or metric trigger into scored results, including the per-layer fan-out a cascade performs across a bound group. You register a custom strategy on both the engagement and metric registries so it appears for programs and distributors. See [Calculation Strategies](/documentation/extensions/calculation-strategies). Eligibility resolvers decide which programs or distributors a collaborator can earn from. This is the seam that makes a group binding earnable: binding a program to a group registers it through a group-bound resolver, and the eligibility service unions every registered resolver's results. See [Eligibility Resolvers](/documentation/extensions/eligibility-resolvers). ## How does Siren's event-driven architecture work? Siren's core processes domain events, not platform-specific data. Your extension's job is to translate between the two. | WordPress Hook | Domain Event | Siren Core Handler | |---|---|---| | `woocommerce_new_order` | `SaleTriggered` | `ProcessSale` | | `gform_after_submission` | `LeadTriggered` | `ProcessLead` | | `edd_complete_purchase` | `TransactionCompleted` | `FinalizeTransaction` | At the platform level, WordPress fires an action. Your extension's transformer converts the hook's arguments into a platform-agnostic domain event. Then handler classes in Siren's core react to those domain events, working identically regardless of which extension triggered the event. This is why the distinction between [event bindings](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding) and [listeners](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners) matters. Event bindings connect platform hooks to domain events — they define what triggers an event. Listeners connect domain events to handler classes — they define what reacts to an event. Most third-party integrations only need event bindings because Siren's core already has listeners for the standard domain events. You add listeners when your extension needs to react to events that core doesn't handle by default. ## How do extensions access services? Extensions do not instantiate their own dependencies. PHPNomad's DI container resolves everything: ```php // In a transformer callback inside getEventBindings() $callback = fn($orderId) => $this->container->get( TransactionTransformerService::class )->getSaleTriggeredEvent($orderId); ``` The container auto-wires constructor dependencies. If your transformer's constructor asks for a `MappingDatastore` and a `LoggerStrategy`, the container provides them automatically. You only need to register [class definitions](https://phpnomad.com/core-concepts/bootstrapping/creating-and-managing-initializers/) when you are providing a new concrete implementation for an interface that other code resolves through the container. ## Getting started The fastest way to build an extension is to clone the extension template: **https://github.com/Novatorius/siren-extension-template** The template provides a working skeleton with all the interfaces wired up, a sample transformer, adapter, and handler. See the [Quickstart guide](/documentation/extensions/quickstart) and [Template Reference](/documentation/extensions/template-reference) for detailed walkthroughs. ## Extension Template Reference Source: https://www.sirenaffiliates.com/documentation/extensions/template-reference File-by-file guide to the siren-extension-template repository. import EventFlow from "@/components/content/EventFlow.astro"; # Extension Template Reference A file-by-file guide to the Siren extension template repository at [https://github.com/Novatorius/siren-extension-template](https://github.com/Novatorius/siren-extension-template). Each section explains what the file does, what to customize, and when you might remove it. ## Directory Structure ``` siren-extension-template/ plugin.php # WordPress bootstrap composer.json # Autoloading and dependencies lib/ Integration.php # Main extension class Transformers/ SaleTriggeredTransformer.php # Hook-to-event bridge Adapters/ OrderToTransactionDetailsAdapter.php # Data format conversion Handlers/ AdminHandler.php # Event listener with DI ``` --- ## plugin.php (WordPress Bootstrap) This is the entry point. WordPress reads its header comment for plugin metadata, and the body registers the extension with Siren. ### Anatomy ```php new Integration()); }, 0); ``` ### What each section does The plugin header is standard WordPress metadata — update the name, description, author, and URI. The `Requires PHP: 8.1` constraint is required because Siren uses PHP 8.1 features. The root constant `SIREN_YOUR_EXTENSION_ROOT` stores `__FILE__`, which your `Integration::getRootPath()` returns. Siren uses this to locate templates and resources relative to your plugin. The guard clause `if (! function_exists('add_action'))` prevents the file from executing outside WordPress. Some extensions use `if (! defined('ABSPATH'))` instead — both work. This guard is mandatory. The `siren_ready` hook at priority `0` calls `Extensions::add()` with a unique string ID and a callable factory that returns your `Integration` instance. The factory pattern is intentional — your class is not instantiated until Siren is ready to process extensions, keeping registration lightweight and avoiding loading classes before their dependencies are available. ### What to Customize - All metadata in the plugin header - The constant name (match your extension name) - The `use` statements (match your namespace) - If your extension needs its own Composer autoloader, add `require_once __DIR__ . '/vendor/autoload.php';` before the `siren_ready` hook --- ## composer.json (Namespace and Dependencies) ### Anatomy ```json { "name": "novatorius/siren-extension-template", "autoload": { "psr-4": { "Novatorius\\SirenExtensionTemplate\\": "lib/" } }, "require": { "php": ">=8.1" } } ``` ### What to Customize Update the `name` field to your vendor/package name. Map your root namespace to the `lib/` directory in the PSR-4 section — every class in `lib/` must use this namespace as its root. Add any Composer dependencies to the `require` section, but you do not need to require Siren itself — it will already be loaded when your extension runs. Do not add `phpnomad/*` packages to your `require` section. Siren bundles these, and requiring them separately risks version conflicts. Your extension's classes can use [PHPNomad](https://phpnomad.com/) interfaces (like `CanHandle`, `HasEventBindings`, etc.) because they are already loaded by Siren's autoloader. --- ## lib/Integration.php (Main Extension Class) This is the heart of your extension. It implements the interfaces that tell Siren what your extension does and how to load it. ### Interface Choices The template implements all five common interfaces: ```php class Integration implements Extension, // Required -- identifies this as a Siren extension HasEventBindings, // Maps WordPress hooks to domain events HasListeners, // Attaches handlers to domain events HasLoadCondition, // Gates loading behind a condition check CanSetContainer, // Receives the DI container Loadable // Runs imperative setup after all bindings { use HasSettableContainer; // Satisfies CanSetContainer } ``` **You do not need all of them.** Only implement what your extension uses: | Building... | Required Interfaces | Optional Interfaces | |-------------|-------------------|-------------------| | Third-party integration | `Extension`, `HasEventBindings`, `HasLoadCondition`, `CanSetContainer` | `Loadable`, `HasListeners` | | Feature extension | `Extension`, `HasListeners`, `HasLoadCondition`, `CanSetContainer` | `Loadable`, `HasEventBindings` | | Minimal extension | `Extension`, `HasLoadCondition` | Everything else | ### Method Reference #### Get ID Static method. Returns the unique short identifier for this extension. ```php public static function getId(): string { return 'my_plugin'; } ``` Conventions: - Lowercase, underscores or short abbreviations - Must be unique across all installed extensions - Used as the module ID for template path resolution (`my_plugin::path/to/template`) - Existing IDs in Siren: `wc`, `edd`, `gf`, `lifterlms`, `learndash`, `nc`, `wordpress-core` #### Root path Returns the filesystem path to the extension's root. Always return the constant defined in `plugin.php`: ```php public function getRootPath(): string { return SIREN_MY_PLUGIN_ROOT; } ``` #### Name and description Human-readable name and description, displayed in Siren's admin: ```php public function getName(): string { return 'My Plugin'; } public function getDescription(): string { return 'Integrates My Plugin with Siren Affiliates'; } ``` #### Activation check Checks whether the extension's dependencies are available. This is called by Siren's admin even when the extension has not loaded. The most common pattern is a class existence check: ```php public function canActivate(): bool { return class_exists('MyPluginMainClass'); } ``` For extensions that do not depend on a third party, return `true` or check for a configuration requirement. #### Load condition Called during the loading lifecycle. If this returns `false`, the extension is skipped entirely. Usually delegates to `canActivate()`: ```php public function shouldLoad(): bool { return $this->canActivate(); } ``` Override this separately when you need a different condition for "can it work?" vs. "should it load right now?" For example, you might check a feature flag or settings value. #### Feature support Declares which Siren features this extension provides: ```php use Siren\Extensions\Core\Enums\Features; public function getSupports(): array { return [ Features::Coupons, Features::ManualOrdering, ]; } ``` Siren uses this to enable/disable UI features. For example, the coupon management UI only appears if at least one active extension declares `Features::Coupons`. #### Active state Returns whether the extension has finished loading. Set this in `load()`: ```php protected bool $isActive = false; public function getIsActive(): bool { return $this->isActive; } public function load(): void { $this->isActive = true; // ... other setup } ``` #### Event bindings Maps WordPress hooks to Siren domain events. See the [Transformer Pattern](#the-transformer-pattern) section below for the full format. #### Listeners Maps domain events to handler classes. See the [Handler Pattern](#the-handler-pattern) section below for the full format. #### Load method Runs last in the interface processing order. Use this for imperative setup that does not fit the declarative interfaces. Examples include registering admin services and adding WordPress filters. ```php public function load(): void { $this->isActive = true; add_action('plugins_loaded', fn() => $this->container->get( CouponAdminService::class )->init()); } ``` --- ## lib/Transformers/ (The Transformer Pattern) Transformers are the bridge between platform-specific hooks and platform-agnostic domain events. They are unique to third-party integrations. ### When to Use Use a transformer when you need to convert a WordPress hook's arguments into a Siren domain event. Every entry in `getEventBindings()` that includes a `'transformer'` key points to a transformer method. ### How It Works ### Structure ```php class SaleTriggeredTransformer { // Dependencies injected by the DI container protected OpportunityLocatorService $opportunityLocator; protected VisitorOpportunity $visitorLocators; protected OrderToTransactionDetailsAdapter $detailsAdapter; public function __construct( OpportunityLocatorService $opportunityLocator, VisitorOpportunity $visitorLocators, OrderToTransactionDetailsAdapter $detailsAdapter ) { $this->opportunityLocator = $opportunityLocator; $this->visitorLocators = $visitorLocators; $this->detailsAdapter = $detailsAdapter; } public function getSaleTriggeredEvent(int $orderId): ?SaleTriggered { // Platform-specific: get the customer from your plugin $userId = my_plugin_get_order_user_id($orderId); // Platform-agnostic: find the affiliate opportunity $opportunity = $this->opportunityLocator->locateUsing( ...$this->visitorLocators->build($userId) ); if (!$opportunity) { return null; // No affiliate involved -- skip } // Build the domain event return new SaleTriggered( $opportunity->getId(), $this->detailsAdapter->toArray($orderId), 'my_plugin', (string) $orderId, 'my_plugin_order' ); } } ``` ### Key Rules Transformers are resolved from the DI container, so their constructors are auto-wired — just declare your dependencies as constructor parameters. Return `null` to skip if the hook fired but there is no affiliate opportunity or the event should not be tracked. The transformer method's parameter list must match the WordPress hook's arguments: if `my_plugin_order_completed` passes `($orderId, $orderData)`, your transformer must accept those same parameters. You can use the same transformer for multiple hooks bound to the same event type as long as the argument format is compatible — create separate transformers when formats differ. ### How Transformers Connect to Event Bindings In your `Integration::getEventBindings()`: ```php $callback = fn($orderId) => $this->container->get( SaleTriggeredTransformer::class )->getSaleTriggeredEvent($orderId); return [ SaleTriggered::class => [ ['action' => 'my_plugin_order_completed', 'transformer' => $callback], ], ]; ``` The closure wraps the container resolution. This ensures the transformer is only instantiated when the hook actually fires, not when bindings are registered. ### When to Delete If your extension is a **feature extension** that does not integrate a third party, you do not need transformers. Delete the `lib/Transformers/` directory and remove `HasEventBindings` from your `Integration` class. --- ## lib/Adapters/ (The Adapter Pattern) Adapters convert data from one format to another. In the extension context, they typically convert platform-specific data structures into Siren's standardized formats. ### When to Use Use an adapter when you need to reshape data from the integrated plugin into a format Siren understands. The most common case is converting order/payment data into Siren's transaction detail format. ### Structure ```php class OrderToTransactionDetailsAdapter { protected FloatToIntPriceAdapter $priceAdapter; public function __construct(FloatToIntPriceAdapter $priceAdapter) { $this->priceAdapter = $priceAdapter; } public function toArray(int $orderId): array { $order = my_plugin_get_order($orderId); $result = []; foreach ($order->getItems() as $item) { $result[] = [ 'name' => $item->getName(), 'description' => $item->getQuantity() . ' X ' . $item->getName(), 'type' => 'product', 'value' => $this->priceAdapter->toInt($item->getPrice()), 'quantity' => $item->getQuantity(), 'units' => $order->getCurrency(), 'externalId' => (string) $item->getId(), ]; } return $result; } } ``` ### Key Rules Adapters are pure data converters — they should not contain business logic, side effects, or event dispatching. Always use `FloatToIntPriceAdapter` for price conversions because Siren stores monetary values as integers in the smallest currency unit. Adapters are injected into transformers, which call them to get formatted data and then pass that data into the domain event constructor. ### Transaction Detail Format The standard format for transaction line items: ```php [ 'name' => 'Product Name', // Required 'description' => '2 X Product Name', // Required 'type' => 'product', // Required: product|shipping|tax|fee|discount 'value' => 2999, // Required: price in cents (integer) 'quantity' => 2, // Required: number of units 'units' => 'USD', // Required: currency code 'externalId' => '123', // Optional: ID in source system 'attributes' => [ // Optional: extra metadata 'collaborators' => [1, 2], // Collaborator IDs bound to product 'categories' => ['digital'], // Product categories 'sku' => 'PROD-001', // SKU ], ] ``` ### When to Delete If your extension does not transform external data into Siren formats, you do not need adapters. Feature extensions that only listen to domain events typically have no adapters. Delete the `lib/Adapters/` directory. --- ## lib/Handlers/ (The Handler Pattern) Handlers (also called listeners) react to domain events. They implement [`PHPNomad\Events\Interfaces\CanHandle`](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners) and are registered through the `HasListeners` interface on your `Integration` class. ### When to Use Use a handler when your extension needs to **react** to something that happens in Siren or another extension. Common use cases: - Initialize platform-specific services when Siren is ready (`Ready` event) - Perform cleanup when a transaction is refunded - Sync data to an external system when an event fires ### Structure ```php service = $service; } public function handle(Event $event): void { // React to the event $this->service->doSomething(); } } ``` ### How Handlers Connect to Listeners In your `Integration::getListeners()`: ```php use PHPNomad\Core\Events\Ready; public function getListeners(): array { return [ Ready::class => [AdminHandler::class], // Or a single handler (not wrapped in array): // Ready::class => AdminHandler::class, ]; } ``` The format is: ```php [ EventClass::class => HandlerClass::class, // or for multiple handlers on the same event: EventClass::class => [ FirstHandler::class, SecondHandler::class, ], ] ``` ### Key Rules Handlers are resolved from the DI container with auto-wired constructor dependencies, which is why the `Integration` class only declares class names rather than instantiating handlers directly. The `handle()` method receives the specific event class even though the signature says `Event $event` — type-check with `instanceof` if you need event-specific methods: ```php public function handle(Event $event): void { if ($event instanceof SaleTriggered) { $opportunityId = $event->getOpportunityId(); } } ``` Keep handlers single-purpose: one handler, one job. If you need to do three things when a sale fires, create three handlers. And always catch exceptions in handlers — uncaught exceptions bubble up and can prevent other handlers from running. ### Common Domain Events | Event | When It Fires | |-------|--------------| | `PHPNomad\Core\Events\Ready` | Siren has fully initialized | | `Siren\Commerce\Events\SaleTriggered` | A sale is detected | | `Siren\Commerce\Events\TransactionCompleted` | A transaction is finalized | | `Siren\Commerce\Events\RefundTriggered` | A refund is processed | | `Siren\Commerce\Events\RenewalTriggered` | A subscription renewal occurs | | `Siren\Commerce\Events\CouponApplied` | An affiliate coupon is used | | `Siren\Commerce\Events\LeadTriggered` | A lead/form submission occurs | | `Siren\Extensions\Core\Events\ExtensionInit` | Extensions are being initialized | ### When to Delete If your extension only **produces** domain events (via event bindings and transformers) and does not need to **react** to them, you do not need handlers. Delete the `lib/Handlers/` directory and remove `HasListeners` from your `Integration` class. --- ## Choosing What to Keep Not every extension needs every pattern. Here is a decision guide: ### Integration Extension (e.g., WooCommerce, EDD) Typical minimum: - `plugin.php`. Always required. - `composer.json`. Always required. - `lib/Integration.php`. Implements `Extension`, `HasEventBindings`, `HasLoadCondition`, `CanSetContainer`, `Loadable`. - `lib/Transformers/`. One transformer per event type you produce. - `lib/Adapters/`. One adapter per data format you convert. - `lib/Handlers/`. Only needed if you also react to events (e.g., initializing a third-party addon). ### Feature Extension (e.g., custom reporting, notification service) Typical minimum: - `plugin.php`. Always required. - `composer.json`. Always required. - `lib/Integration.php`. Implements `Extension`, `HasListeners`, `HasLoadCondition`, `CanSetContainer`, `Loadable`. - `lib/Handlers/`. One handler per event you react to. - No `lib/Transformers/`. You are not bridging platform hooks. - No `lib/Adapters/`. Only needed if you convert data between formats. ### Minimal Extension (e.g., adds admin UI only) Bare minimum: - `plugin.php`. Always required. - `composer.json`. Always required. - `lib/Integration.php`. Implements `Extension`, `HasLoadCondition`, `Loadable`. - Everything goes in `load()`. Register admin hooks, enqueue scripts, etc. - No transformers, adapters, or handlers --- ## Extending Beyond the Template The template covers the most common patterns, but extensions can also use these interfaces on their `Integration` class: | Interface | Purpose | |-----------|---------| | `HasClassDefinitions` | Register DI container bindings (interface to concrete mappings) | | `HasControllers` | Register REST API routes | | `HasCommands` | Register CLI commands | | `HasUpdates` | Register database migration/upgrade routines | | `HasFacades` | Register facade instances | | `HasMutations` | Attach mutation callbacks | | `HasTaskHandlers` | Register background task handlers | These follow the same pattern: implement the interface, return the configuration array from the required method, and the loader processes it automatically during the interface processing order. --- ## Related Documentation - [Extension Architecture Overview](/documentation/extensions/architecture-overview). How the extension system works. - [Quickstart: Your First Extension](/documentation/extensions/quickstart). Step-by-step tutorial. ## Feature Flags & getSupports() Source: https://www.sirenaffiliates.com/documentation/extensions/feature-flags The Features enum, getSupports(), and how core conditionally enables behavior. # Feature Flags & getSupports() Siren's extension system uses feature flags to declare what capabilities each integration provides. Core uses these flags to conditionally enable engagement triggers, conversion types, metric strategies, and UI elements. This prevents the system from offering functionality that no active extension can fulfill. ## What is the Features enum? All feature flags are defined in `Siren\Extensions\Core\Enums\Features`: ```php query( (new ExtensionQueryBuilder())->isActive()->supports($feature, ...$features) )); } } ``` This method returns `true` if **at least one active extension** supports **all** of the specified features. ### Feature Checks in Engagement Trigger Registration The most visible use of feature flags is in engagement trigger registration listeners. Here is the core registration: ```php class RegisterCoreEngagementTriggerStrategies implements CanHandle { protected ExtensionRegistryService $extensionRegistryService; public function __construct(ExtensionRegistryService $extensionRegistryService) { $this->extensionRegistryService = $extensionRegistryService; } public function handle(Event $event): void { if ($event instanceof EngagementTriggerRegistryInitiated) { // Always available -- no feature flag needed $event->addStrategy(ReferredSiteVisit::class); $event->addStrategy(Manual::class); // Only available if an active extension supports Coupons $this->addStrategyIfSupported($event, BoundCouponUsed::class, Features::Coupons); } } protected function addStrategyIfSupported( EngagementTriggerRegistryInitiated $event, string $strategy, string $feature, string ...$features ) { if ($this->extensionRegistryService->extensionsSupportFeatures($feature, ...$features)) { $event->addStrategy($strategy); } } } ``` The Essentials tier adds more feature-gated strategies: ```php class RegisterEssentialsEngagementTriggerStrategies implements CanHandle { public function handle(Event $event): void { if ($event instanceof EngagementTriggerRegistryInitiated) { $this->addStrategyIfSupported($event, BoundPostUsed::class, Features::Posts); $event->addStrategy(CollaboratorProductSold::class); $this->addStrategyIfSupported($event, CollaboratorFormSubmitted::class, Features::Forms); $this->addStrategyIfSupported($event, CourseCompleted::class, Features::Courses); $this->addStrategyIfSupported($event, LessonCompleted::class, Features::Lessons); } } } ``` ### Feature Checks in Metric Trigger Registration The same pattern appears for distribution metrics: ```php class RegisterCoreMetricTriggerStrategies implements CanHandle { public function handle(Event $event): void { if ($event instanceof MetricTypeRegistryInitiated) { // Always available $event->addStrategy(ReferredSiteVisit::class); $event->addStrategy(BoundPostUsed::class); $event->addStrategy(CollaboratorProductSold::class); $event->addStrategy(Manual::class); // Feature-gated $this->addStrategyIfSupported($event, CollaboratorFormSubmitted::class, Features::Forms); $this->addStrategyIfSupported($event, BoundCouponUsed::class, Features::Coupons); $this->addStrategyIfSupported($event, CourseCompleted::class, Features::Courses); $this->addStrategyIfSupported($event, LessonCompleted::class, Features::Lessons); } } } ``` ## How do I query extensions with complex conditions? For more complex queries beyond simple feature support checks, use `ExtensionQueryBuilder`: ```php $extensions = $extensionRegistryService->query( (new ExtensionQueryBuilder()) ->isActive() ->supports(Features::Coupons) ); // Other query methods: $query = (new ExtensionQueryBuilder()) ->isActive() // Only active extensions ->isInactive() // Only inactive extensions ->supports('coupons') // Must support this feature ->doesNotSupport('courses') // Must NOT support this feature ; ``` ## How do I add feature-conditional logic in my extension? ### Checking Features at Runtime If your extension needs to conditionally behave based on what other extensions support, inject `ExtensionRegistryService`: ```php class MyListener implements CanHandle { protected ExtensionRegistryService $extensions; public function __construct(ExtensionRegistryService $extensions) { $this->extensions = $extensions; } public function handle(Event $event): void { if ($this->extensions->extensionsSupportFeatures(Features::Coupons)) { // Coupon-related logic } if ($this->extensions->extensionsSupportFeatures(Features::Courses, Features::Lessons)) { // Only if BOTH courses AND lessons are supported } } } ``` ### Declaring Your Own Features If your extension provides a capability that core should know about, add the feature to your `getSupports()` return. The feature constants in `Features` are the currently recognized set, but the system uses strings internally, so custom features are possible if you also write the listeners that check for them. ### Conditional Event Bindings WooCommerce demonstrates conditional event bindings based on available features: ```php public function getEventBindings(): array { $triggers = [ SaleTriggered::class => [ ['action' => 'woocommerce_new_order', 'transformer' => $saleTransformerCallback], ], ]; // Only add renewal bindings if WC Subscriptions is available if (class_exists(WC_Subscription::class)) { $triggers[RenewalTriggered::class] = [ ['action' => 'woocommerce_subscription_renewal_payment_complete', 'transformer' => $renewalTransformerCallback], ]; } return $triggers; } ``` ## How does the feature flag lifecycle work? When an extension loads, `canActivate()` checks whether the third-party plugin is present. If it passes, `load()` runs and the extension becomes active. At that point `getSupports()` declares its capabilities. During registry events, Siren's core listeners call `extensionsSupportFeatures()` to decide what strategies to register, and only strategies whose feature requirements are met appear in the system. The program creation UI then reflects only the engagement types, conversion types, and metrics that have registered strategies. The practical effect is that installing WooCommerce automatically enables coupon-based engagement tracking, renewal conversion types, and manual ordering support. Uninstalling it removes those options. No configuration needed. ## Fixed calculation Source: https://www.sirenaffiliates.com/documentation/calculation-strategies/fixed The default. Emit a single score for the triggering collaborator at a configured value. Fixed is the default calculation strategy. One trigger produces one credit, and the credit lands on the collaborator who caused the trigger. No fanout, no layers, no walking a group, just a single row at a single configured value. If you've used Siren without thinking about calculation strategies, you've been using Fixed. ## How it works A trigger fires the engagement type (or metric type) the calculation is attached to. Fixed emits one result that credits the triggering collaborator at the value you configured. That's the whole mechanic. The triggering collaborator is the person Siren resolves from the event itself: the affiliate whose link drove the sale, the user who completed the action, the account tied to the metric being recorded. Fixed doesn't look at any group around them. It doesn't check structure. It doesn't care whether the collaborator belongs to a [collaborator group](/documentation/general/what-are-collaborator-groups) at all. Because Fixed only ever emits one row per trigger, it carries no capability requirements. It works with every structure and shows up in the picker for every program and distributor. ## When to use it Fixed is the default, and most programs never need anything else. Reach for it whenever the credit should land on exactly one person, the one who triggered the event. That covers a standard per-sale commission, where an affiliate refers a sale and gets paid, as well as per-action point awards, where someone completes a tracked action and earns the points for it. It also covers any single-recipient payout where no upline or downline is involved. If you want the trigger to credit more than one person (the referrer's sponsor, a team lead, members further down a chain), you want a cascade instead. See [upline cascade](/documentation/calculation-strategies/upline-cascade) and [downline cascade](/documentation/calculation-strategies/downline-cascade), or read [choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy) for the full comparison. ## Configuration Fixed has one field. - `value` (integer): the score awarded to the triggering collaborator. How that score becomes a payout depends on the structure it feeds. On a program, that is the program's [incentive structure](/documentation/incentive-structures/choosing-an-incentive-structure). On a distributor, it is the [distribution structure](/documentation/distribution-structures/choosing-a-distribution-structure). With a direct, fixed-per-incentive structure the score is taken literally, so the value you set is the amount paid out. With a [performance-weighted pool](/documentation/distribution-structures/performance-weighted-pool), the score becomes a weight that sets the collaborator's share of the pool relative to everyone else's contributions for the period. Either way, Fixed itself just emits the number you configured. What that number means downstream is the structure's job. ## Fixed Per Product Source: https://www.sirenaffiliates.com/documentation/incentive-structures/fixed-per-product An incentive structure that pays a flat fee per unit of a product sold. Multiplies by quantity. Fixed Per Product is an incentive structure that pays a flat fee for each unit of a qualifying product sold. Unlike [Fixed Per Transaction](/documentation/incentive-structures/fixed-per-transaction), which pays once per conversion, this structure multiplies the flat fee by the quantity of units in the transaction. When a customer converts, Siren counts qualifying units across the transaction's line items and creates an obligation for the unit count times the configured per-unit amount. [Line item filters](/documentation/general/line-item-filters) determine which products count. ## How it works If the incentive is set to $5 per unit and a customer buys 8 units of a qualifying product in a single order, the commission is $40. If they buy 1 unit, it's $5. Quantity drives the payout. This structure is a good fit when unit profit is roughly constant. If each product unit yields the same margin, a flat per-unit commission gives you predictable cost-per-sale math. You can calculate exactly what a collaborator earns for any cart composition without worrying about fluctuating percentages. ## Where this works This works best for stores with uniform product pricing and stable per-unit margins (books, standardized supplements, consumer electronics with fixed retail prices). It also suits subscription services with flat monthly pricing, where you can easily compute a sustainable per-signup payout based on average customer lifetime value. The [fixed rate affiliate program](/recipes/fixed-rate-affiliate-program) recipe uses a flat dollar payout per sale, which works well whenever the business math on each unit is predictable enough to set a sustainable commission up front. ## When to avoid this If you sell products at wildly different price points and want affiliates to focus on higher-value items, use [Percentage of Transaction](/documentation/incentive-structures/percentage-of-transaction) instead. A flat per-unit fee treats a $10 product and a $500 product the same, which usually doesn't match your actual margin on either. It's also not a fit for service businesses or programs where the thing being sold isn't a discrete product unit. If you're paying for subscriptions, leads, or single-transaction outcomes without a per-unit concept, Fixed Per Transaction or Percentage of Transaction will fit better. ## Fixed Per Transaction Source: https://www.sirenaffiliates.com/documentation/incentive-structures/fixed-per-transaction An incentive structure that pays a flat fee per transaction, regardless of transaction size or item count. Fixed Per Transaction is an incentive structure that pays a flat fee for each qualifying [transaction](/documentation/general/what-are-transactions), regardless of how large the transaction is or how many items it contains. One conversion equals one fixed payout. When a customer converts, Siren creates an obligation for the configured flat amount. The transaction could be $10 or $10,000. The commission is the same. ## How it works If the incentive is set to $25 per transaction, every qualifying conversion generates a $25 obligation. A customer who buys one $50 product and a customer who buys ten $500 products both earn the collaborator the same $25. [Line item filters](/documentation/general/line-item-filters) still apply for determining whether a transaction qualifies at all, but once it does, the payout is flat. ## With a cascade calculation strategy The flat-fee-per-conversion framing above assumes the default Fixed calculation strategy, where one conversion produces one payout for the collaborator who triggered it. When a program instead uses an [Upline Cascade or Downline Cascade](/documentation/calculation-strategies/choosing-a-calculation-strategy), the incentive structure sits downstream of the cascade. The cascade walks the bound [collaborator group](/documentation/general/what-are-collaborator-groups) and emits a per-layer score for each credited peer, and the incentive structure then turns each of those scores into a payout. So a single conversion can produce a payout per layer rather than one flat payout. See [what is a cascade](/documentation/general/what-is-a-cascade) for how per-layer scores feed the incentive structure. ## Where this works Flat-fee commissions work best when the value of a conversion is roughly uniform and you want a predictable cost per sale. They're also the right choice when you're rewarding actions that don't have a clean transaction value, like lead submissions. The [refer a friend program](/recipes/refer-a-friend-program) recipe pays a flat reward for each referred customer, which keeps the program simple for participants and gives you predictable marketing costs. The [pay per lead affiliate program](/recipes/pay-per-lead-affiliate-program) recipe applies the same flat-fee model to form submissions, paying for leads rather than sales. This structure also outperforms percentage-based commissions when your store runs heavy discounts. Percentages shrink with every discount, which can frustrate affiliates who still did the work of driving the sale. A flat fee insulates them from price changes. ## When to avoid this If sale sizes vary dramatically and you want affiliates to be motivated by larger carts or premium products, use [Percentage of Transaction](/documentation/incentive-structures/percentage-of-transaction) instead. A flat fee gives affiliates no reason to push higher-value products over cheaper ones. It's also a poor fit for programs where each sold unit has roughly the same profit margin but transactions vary in quantity. In that case, [Fixed Per Product](/documentation/incentive-structures/fixed-per-product) scales the payout with volume while keeping the per-unit economics predictable. ## Flat structure Source: https://www.sirenaffiliates.com/documentation/collaborator-group-structures/flat An unordered collection of collaborators with no hierarchy and no cascade behavior. Flat is a [collaborator group](/documentation/general/what-are-collaborator-groups) structure with no internal ordering or hierarchy. Members live as a plain list, and every member is treated as an equal peer. ## How it works A flat group is exactly what it sounds like: a bag of collaborators with no upline, no downline, and no layers. There is no chain to walk and no tree to traverse, so cascades have nothing to spread across, and credit for an engagement stays with the collaborator who earned it. Binding a flat group still does a job, and it is the main reason to use one: it controls who is eligible to earn. A collaborator who is a member of the bound group becomes eligible for that program or distributor, so a flat group acts as an allow-list. This eligibility is added to Siren's direct binding rather than replacing it, so a collaborator qualifies either by being in the bound group or through a direct binding, and neither path masks the other. If the bound group is the only path you use, only its members can earn. This shapes what the calc picker shows you. On the Programs Edit and Distributors Edit screens, Siren reads the bound group's structure and filters calc options to ones that match its [capabilities](/documentation/general/calc-capability-matching). Flat groups don't advertise `hasLayer`, so cascade calcs are hidden. You'll see [Fixed](/documentation/calculation-strategies/fixed) and any other layer-agnostic calc, and that's it. What flat changes is not who is eligible, since the bound group still decides that, but which calculations are available. With no layers to walk, only single-collaborator calculations like Fixed remain, so credit lands on the one collaborator who fired the engagement rather than spreading up or down a chain. The membership check that decides whether a triggering collaborator belongs to the group runs the same way it does for any other structure. ## When to use it Flat is where most programs begin, because it makes no assumptions about who reports to whom or who sits above whom in an org chart. When you only need to gather collaborators into one group and credit each one for their own work, flat covers it without any extra setup. That fits a sales team where everyone earns the same commission structure regardless of seniority, a reseller pool whose members all work the same deal, a curated marketplace or invite-only roster where only the vetted members on the list can earn, and an affiliate network without referral chains where every affiliate earns on their own engagements and nothing else. Starting flat also keeps your options open, since you can switch the group to a linear chain or parent-child structure later and its members carry over. The per-member metadata that orders those members does not, so after the switch you need to set each member's `position` (linear chain) or parent (parent-child) before a cascade reads the group the way you expect. If you find yourself wishing a single sale could pay out across multiple collaborators based on their position in an org, that's the signal to look at [linear chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) instead. See [Choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) for the full comparison. ## Configuration None. Flat is the default structure for a collaborator group. Create a group, pick "Flat" as the structure, add members, and you're done. There are no per-member position fields, no parent references, no per-layer args to set. Member ordering in the admin UI is alphabetical for readability and has no effect on behavior. Adding, removing, suspending, or deleting members works the same as any other group: it changes who's in the bag, and that's all. ## Form Submitted Source: https://www.sirenaffiliates.com/documentation/general/form-submitted When a customer submits a form that is associated with a collaborator, the Form Submitted event is triggered. When a customer submits a form that is associated with a [collaborator](/documentation/general/what-is-a-collaborator), the "Form Submitted" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the form submission to the collaborator who owns that form. This engagement type is designed for lead generation programs where the valuable action is a form submission rather than a purchase. A collaborator might have a contact form, a quote request form, or an application form on your site. When a visitor fills it out, the collaborator gets credit. Someone Submits a Form A visitor submits a form that is associated with a collaborator. The form integration (Gravity Forms or Ninja Forms) detects the submission and identifies which collaborator owns the form. Engagement Created If there isn't already an engagement for the collaborator and the visitor, it gets created now. Engagement Points Added Points get added to the engagement, depending on the value set for the engagement in the [program](/documentation/general/what-are-programs). ## How forms are linked to collaborators Forms are associated with collaborators through the same product ownership system used for other content types. In Gravity Forms and Ninja Forms, you assign a collaborator as the owner of a specific form. When that form receives a submission, Siren attributes it to the owning collaborator. This works alongside the [lead conversion type](/documentation/resource-reference/enums-and-constants#conversion-types). When a form submission triggers a lead conversion, the collaborator earns their reward based on the program's incentive structure. This makes it possible to pay collaborators a fixed amount per lead without requiring a purchase. ## When to use this engagement type Form submissions are useful when the goal of your program is lead generation rather than direct sales. A few common scenarios where this fits well include service businesses that pay collaborators for qualified leads, membership sites that reward collaborators for driving signups, and educational platforms that compensate partners for driving course enrollments through application forms. This engagement type requires a form integration, either Gravity Forms or Ninja Forms. ## Frequently Asked Questions Source: https://www.sirenaffiliates.com/documentation/general/frequently-asked-questions Answers to the most common questions about setting up Siren, how tracking works, and how payouts get handled. # Setup and getting started ## What integrations does Siren support? Siren works with WooCommerce, Easy Digital Downloads, LifterLMS, LearnDash, NorthCommerce, and Gravity Forms. All of these ship with both the Lite and Essentials tiers, so there's no extra purchase required to use any of them. As long as the supported plugin is active on your WordPress site, Siren detects it and turns on the matching features automatically. Not every integration supports every feature. Refunds, coupon tracking, and renewal handling vary depending on what each commerce plugin exposes. The [integration feature matrix](/documentation/general/integration-feature-matrix) has a side-by-side comparison so you can see exactly which features apply to the plugin you're using. ## How many sites can I install Siren on? That depends on which tier you buy. Each price point allows a different number of activations, and you can review the current options on the [pricing page](/pricing/wordpress). If you outgrow your tier later, you can upgrade and pick up the additional sites without losing any of your existing data. ## Do I need a developer to set up Siren? No. Siren is built so a non-technical store owner can install it, pick a recipe, and start running an affiliate program the same afternoon. Recipes are pre-built configurations that create programs, program groups, and distributors with sensible defaults, so you don't have to understand every concept before you can launch. That said, Siren is also extensible from top to bottom if you do have a developer. The whole system is event-driven, which means custom triggers, custom incentive structures, and integrations with other tools are all possible without forking the plugin. ## What's the difference between Lite and Essentials? Lite is the free tier and covers the core affiliate workflow: tracking referrals, creating conversions, and paying out commissions. Lite also covers coupon code tracking, so a collaborator can be credited through a discount code instead of a link. Essentials unlocks the more advanced features like distributors (scheduled payouts based on aggregate performance), lead and per-product rewards, and the full set of program structures. Both tiers include every commerce integration, so the difference is about how much of Siren's reward modeling you can use, not which platforms you can connect. ## Can I use Siren without WordPress? Not directly. Siren is a WordPress plugin, and it relies on WordPress and its commerce integrations for everything from order detection to user management. If you don't run WordPress, Siren isn't the right fit today. # Concepts ## What's the difference between a program and a distributor? A [program](/documentation/general/what-are-programs) is tied to a single transaction. A customer buys something, the program matches that sale to a collaborator, and an obligation gets created right away. Affiliate commissions, sales commissions, and product royalties all fit this shape. A [distributor](/documentation/general/what-are-distributors) is tied to a schedule instead of a transaction. It tracks aggregate performance over a period of time (a month, a quarter, whatever you set) and then pays out based on that performance when the period ends. Profit shares, monthly bonuses, and "top performer wins" structures all use distributors. If you can't tie the reward to one specific sale, you probably want a distributor. ## What's a program group and when do I need one? A [program group](/documentation/general/what-are-program-groups) bundles overlapping programs together so that only one of them pays out per conversion. The classic example is tiered commission rates. If you want top affiliates to earn 20% and everyone else to earn 10%, you run two programs, and each pays only the affiliates enrolled in it. Put them in a group so a sale that matches both, because an affiliate sits in both tiers during a promotion or two affiliates from different tiers referred the same customer, pays once instead of twice. You only need a program group when programs overlap. If your programs cover entirely different products or audiences, they can run side by side without one. Siren's default behavior is to pay every program it can, and a group is the way to override that when you specifically don't want it. ## Can a collaborator be in multiple programs at once? Yes. A single collaborator can be enrolled in any number of programs and distributors at the same time, and a single conversion can fire multiple programs. If an affiliate refers a customer who buys a book, Siren can pay both the affiliate and the book's author from the same sale without any extra configuration. ## What happens if multiple collaborators contributed to a sale? Each program has a [program structure](/documentation/program-structures/choosing-a-program-structure) that decides who gets credit when more than one collaborator is in the running. Some structures pay only the most recent referrer. Others pay the first one. Some divide the reward proportionally based on engagement scores, and others split it evenly. You pick the structure that matches how you want to attribute credit, and Siren handles the rest. # Tracking and attribution ## How does Siren track who referred a customer? Siren uses three signals. The first is a tracking cookie set when someone clicks a referral link. The second is a tracking ID that can be passed through URL parameters (handy for situations where cookies are unreliable). The third is coupon codes, where the discount code itself acts as the attribution signal at checkout. When a customer converts, Siren looks at all of these signals and creates engagements for whichever collaborators match. ## What if a customer clears their cookies? Cookies are the easiest tracking signal to lose. If a customer clicks a referral link, leaves, comes back a week later through a search engine, and then buys, the original cookie may already be gone. The safety net is [manual attribution](/documentation/general/manual-attribution). An admin can credit any transaction to a collaborator after the fact, and Siren runs the same pipeline it would have run automatically. There's a [step-by-step guide](/documentation/getting-started/manually-attribute-a-transaction) for doing this from the WordPress admin or via the REST API. ## Can affiliates use coupon codes instead of links? Yes. [Coupon code tracking](/documentation/general/coupon-tracking) lets a collaborator promote your products with a discount code instead of (or alongside) a referral link. You create the coupon in your commerce plugin the way you normally would, then assign it to a collaborator through the field Siren adds to the coupon edit screen. When a customer applies the code at checkout, Siren attributes the sale to the right person automatically. This is especially useful for podcast sponsorships, printed materials, and social media posts where a memorable code outperforms a clickable link. Coupon tracking is supported on WooCommerce, Easy Digital Downloads, LifterLMS, and NorthCommerce. ## What if Siren misses a sale entirely? You can still credit the collaborator after the fact. Siren has two tools for this. If the transaction already exists (the order went through, but no one got attributed), use [manual attribution](/documentation/getting-started/manually-attribute-a-transaction) to assign credit to the right collaborator. If the transaction doesn't exist at all (a phone order, an in-person sale, a one-off invoice), you can [create the transaction manually](/documentation/getting-started/creating-transactions-manually) and attribute it from there. ## Does Siren handle refunds automatically? Yes. When an order is refunded, cancelled, failed, or trashed in your commerce plugin, Siren listens for that status change and automatically reverses the affiliate records tied to that order. Conversions get rejected, and any obligations that haven't been paid out yet get rejected too. The full breakdown is in [how refunds work](/documentation/general/how-refunds-work). ## Are partial refunds supported? Not currently. Siren treats refunds as all-or-nothing at the transaction level. If the commerce plugin signals a full refund, Siren reverses the whole conversion. If it's a partial refund and the order status doesn't change, Siren leaves the records alone. There's more detail (and the workaround for handling partial refunds manually) in [how refunds work](/documentation/general/how-refunds-work). # Payments and operations ## How do I pay my collaborators? When you're ready to pay out, you create a fulfillment that tallies what each collaborator is owed across all of their approved obligations. From there, you can export the list as a CSV to feed into your payment processor of choice, or you can mark obligations as paid manually if you've already sent the money another way. The full walkthrough lives in the [paying collaborators guide](/documentation/getting-started/how-to-pay-collaborators). ## Does Siren actually send the payments? No. Siren is the system of record for who's owed what, but it doesn't move money. You handle the actual transfer through your bank, PayPal, Wise, ACH, or whatever else you use, and then mark the payouts as complete in Siren. This keeps Siren focused on tracking and accounting instead of becoming a payment processor. ## What payment methods does Siren accept for buying the plugin itself? Siren accepts payment methods compatible with Stripe, which includes all major credit cards. We don't currently accept PayPal. # Subscriptions and scale ## Does Siren support recurring referrals? Yes. If a referred customer buys a subscription product, Siren can pay the collaborator on each renewal as well as the initial sale. WooCommerce-based stores need the WooCommerce Subscriptions plugin for this to work, and Easy Digital Downloads stores need the EDD Recurring Payments extension. LifterLMS handles renewals natively. The [integration feature matrix](/documentation/general/integration-feature-matrix) shows exactly which integrations support renewals. ## How many collaborators can Siren handle? There isn't a hard limit baked into the plugin. Siren stores collaborators, engagements, conversions, and obligations as standard WordPress records, which means the practical ceiling depends on your hosting environment more than on Siren itself. Stores running into the thousands of collaborators have reported running Siren without issue on reasonable hosting. ## Where can I get help if I'm stuck? The [documentation](/documentation) is the first stop for setup and concept questions. If you'd rather ask a question in plain language, [Beacon](/documentation/getting-started/what-is-beacon) is an AI assistant that knows the entire Siren docs library and can walk you through configuration questions, recipe selection, and troubleshooting. If you still need a human, the contact form on the site reaches the team directly. ## Fulfillment Bulk Actions Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments/bulk-actions Performs batch operations on multiple fulfillments with enforced state transitions. ### Fulfillment Bulk Actions `POST /siren/v1/fulfillments/bulk` Performs an action on multiple fulfillments at once. This endpoint enforces valid state transitions. Invalid transitions are silently skipped. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `process`, `complete`, `fail`, `delete` | | `ids` | integer[] | Yes | Array of fulfillment IDs to act upon | #### Action Behaviors and Valid Transitions | Action | Status Set | Valid From | Auth Required | |---|---|---|---| | `process` | `processing` | `pending` only | Update | | `complete` | `complete` | `processing` only | Update | | `fail` | `failed` | `pending` or `processing` | Update | | `delete` | (record removed) | any status | Delete | Records that do not meet the "Valid From" precondition are silently skipped. The `affected` count in the response reflects only the records that were actually changed. #### Example ```json { "action": "process", "ids": [5, 6, 7] } ``` ```json { "success": true, "affected": 3 } ``` ## FulfillmentCreated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/fulfillment-created Fires when a new fulfillment batch is created, grouping obligations together for disbursement. # FulfillmentCreated A fulfillment represents a single payout run that groups obligations together for disbursement. When Siren creates one of these batches, it fires `FulfillmentCreated` to notify the system that a new payout cycle has begun. The event is identified as `fulfillment_created` and lives in `Siren\Fulfillments\Core\Events\FulfillmentCreated`. ## What does this event carry? The event provides the `Fulfillment` model, which contains the batch's ID, status, and associated metadata. ```php use Siren\Fulfillments\Core\Events\FulfillmentCreated; public function handle(Event $event): void { $fulfillment = $event->getFulfillment(); $status = $fulfillment->getStatus(); } ``` ## What happens next? After the fulfillment is created, the system generates individual payout records for each collaborator who is owed money. Each of those records fires a [PayoutCreated](/documentation/developer-reference/events-payments/payout-created) event. As the fulfillment moves through its lifecycle, status transitions fire [FulfillmentStatusChanged](/documentation/developer-reference/events-payments/fulfillment-status-changed). Listeners might use `FulfillmentCreated` to log the start of a payout run, notify administrators, or prepare external payment gateway connections before the individual payouts begin processing. See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## Fulfillments Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments Batch payout operations — data model, status lifecycle, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; import FlowchartSteps from "@/components/blocks/FlowchartSteps.astro"; # Fulfillments A fulfillment is a batch container that groups together the payouts generated from pending [obligations](/documentation/resource-reference/obligations). When it is time to pay collaborators, the "generate" endpoint collects pending obligations, groups them by collaborator, creates [payout](/documentation/resource-reference/payouts) records for each, and wraps them in a fulfillment batch. The fulfillment then moves through processing states until all payouts are handled. ## The fulfillment object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Status | `status` | `getStatus()` | string | Current status (see lifecycle below) | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the fulfillment batch was created | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the fulfillment was last modified | ## Status lifecycle | Status | Description | |---|---| | `pending` | The fulfillment batch has been generated. Payouts exist but no processing has begun. | | `processing` | Payment processing is underway for the payouts in this batch. | | `complete` | All payouts in the batch have been handled. | | `failed` | The batch encountered an error. Equivalent to a soft delete; a second DELETE request permanently removes the record. | ## Accessing fulfillment data ```bash # List pending fulfillments curl -X GET "https://your-site.com/wp-json/siren/v1/fulfillments?status=pending&fields=id,status,dateCreated" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single fulfillment with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/fulfillments/12?fields=id,status,payoutCount,totalValue,currency" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Fulfillments\Core\Datastores\Fulfillment\Interfaces\FulfillmentDatastore; class FulfillmentReport { protected FulfillmentDatastore $fulfillments; public function __construct(FulfillmentDatastore $fulfillments) { $this->fulfillments = $fulfillments; } public function getPendingFulfillments(): array { return $this->fulfillments->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); } } ``` ```php use Siren\Fulfillments\Core\Facades\Fulfillments; $pending = Fulfillments::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); $fulfillment = Fulfillments::getById(12); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Fulfillments and payouts are normally created by the fulfillment generation pipeline in response to admin actions. If you find yourself creating these records manually, consider whether you should be triggering the generation process instead, which ensures obligations are correctly resolved and all downstream events fire. ## PHP domain methods Both the fulfillment and payout datastores support all shared methods documented in the [introduction](/documentation/resource-reference/introduction). Neither has additional domain-specific methods beyond standard CRUD. ## Extended fields (REST only) | Field | Type | Description | |---|---|---| | `payoutCount` | integer | Total number of payouts in this fulfillment | | `paidCount` | integer | Number of payouts marked as paid | | `unpaidCount` | integer | Number of payouts not yet paid | | `totalValue` | integer | Sum of all payout values in the fulfillment | | `currency` | string | Currency code for this fulfillment's payouts | | `payouts` | array | Nested array of payout objects (detail endpoint only) | ## Relationships Fulfillments sit downstream of [obligations](/documentation/resource-reference/obligations). The generation process collects pending obligations, creates payouts from them, and links each obligation back to its payout via the obligation's `payoutId` field. A fulfillment acts as a batch container for its [payouts](/documentation/resource-reference/payouts). Each payout carries a `fulfillmentId` linking it to its parent, and the fulfillment's extended fields (`payoutCount`, `totalValue`, etc.) are computed aggregates over its child payouts. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## FulfillmentStatusChanged Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/fulfillment-status-changed Fires when a fulfillment's status transitions through its lifecycle (pending, processing, complete). # FulfillmentStatusChanged As a fulfillment moves through its lifecycle, Siren fires `FulfillmentStatusChanged` at each status transition. The event captures both the fulfillment's current state and the status it is transitioning to, giving listeners the context they need to react to specific phases of the payout process. The event is identified as `fulfillment_status_changed` and lives in `Siren\Fulfillments\Core\Events\FulfillmentStatusChanged`. ## What does this event carry? The event provides the `Fulfillment` model and a `newStatus` string. The fulfillment model reflects the batch's state at the time of the event, and `newStatus` tells you where it is headed. A fulfillment typically moves from pending to processing to complete, though the exact transitions depend on the payout strategy in use. ```php use Siren\Fulfillments\Core\Events\FulfillmentStatusChanged; public function handle(Event $event): void { $fulfillment = $event->getFulfillment(); $newStatus = $event->getNewStatus(); } ``` ## When would you use it? This event is useful for triggering notifications or coordinating with external integrations at specific points in the payout cycle. A listener might send an email to administrators when a fulfillment begins processing, or notify an external accounting system when a fulfillment completes. Because the event fires on every status transition, listeners should check `newStatus` to determine whether the transition is one they care about rather than acting on every occurrence. See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## Generate Fulfillments Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments/generate Generates fulfillment batches from pending obligations, creating payout records for each collaborator. ### Generate Fulfillments `POST /siren/v1/fulfillments/generate` Generates fulfillment batches from pending obligations. This is the primary action endpoint that bridges the obligation and payout stages of the pipeline. When called, it collects pending obligations, groups them by collaborator, creates payout records, and wraps them in a fulfillment batch. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `obligationIds` | integer[] | No | Specific obligation IDs to process. If omitted, processes all pending obligations. | #### Examples Send an empty body to process all pending obligations: ```json {} ``` To process specific obligations, pass their IDs: ```json { "obligationIds": [10, 11, 12] } ``` Response: ```json { "success": true, "fulfillments": [ { "id": 6, "status": "pending", "dateCreated": "2026-04-06T12:00:00Z", "dateModified": "2026-04-06T12:00:00Z" } ] } ``` Returns `500` on a database error during generation. ## Generate Referral Code Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/generate-referral-code Generate a unique referral code for use as a collaborator tracking alias. ### Generate Referral Code `GET /siren/v1/collaborators/generate-referral-code` Generates a unique referral code that can be used as a collaborator tracking alias. Useful for pre-generating codes during collaborator creation flows. **Example Response:** ```json { "code": "BCDGHJK" } ``` ## Get Collaborator Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/get-by-id Retrieve a single collaborator by ID with optional field selection. ### Get Collaborator `GET /siren/v1/collaborators/{id}` Returns a single collaborator by ID. Access is granted to administrators and to the collaborator themselves (owner-bound access). When the `fields` parameter is omitted, the following default fields are returned: `id`, `status`, `fullName`, `nickname`, `email`, `referralCode`, `programs`, `distributors`, `createdDate`, `modifiedDate`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | default fields | Comma-separated list of fields to include | **Example Request:** ``` GET /siren/v1/collaborators/7?fields=id,fullName,programs,unfulfilledObligationCount ``` **Example Response:** ```json { "id": 7, "fullName": "Jane Smith", "programs": [ { "id": 1, "name": "Standard Affiliate" }, { "id": 3, "name": "VIP Partners" } ], "unfulfilledObligationCount": 4 } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Get Collaborator Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/get-by-id Returns a single collaborator group by ID, with optional field selection via the standard field-resolver pattern. # Get Collaborator Group `GET /siren/v1/collaborator-groups/{id}` Returns a single collaborator group by ID. When no `fields` parameter is provided, returns the default set (`id`, `name`, `description`, `structure`). Requires authentication and the Read capability on the `CollaboratorGroup` resource. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | `id,name,description,structure` | Comma-separated list of fields to include | **Example Request:** ``` GET /siren/v1/collaborator-groups/12?fields=id,name,description,structure ``` **Example Response:** ```json { "id": 12, "name": "Sales Reps", "description": "Inside sales chain", "structure": "linearChain" } ``` The available fields are whatever field resolvers are registered for the group: `id`, `name`, `description`, and `structure` in core, plus any a plugin adds through the `CollaboratorGroupResolverRegistryInitiated` event. The default is the core set. The `fields` value is filtered against the available set, so a name that has no resolver (for example `dateCreated`, which is not exposed on a group read) is silently dropped from the response rather than returning an error or a null. The `fields` parameter is optional here and defaults to the core set, unlike [List Collaborator Groups](/documentation/resource-reference/collaborator-groups/list), where it is required. **Error Responses:** - `404`. No record found with that ID. - `500`. Database error fetching the collaborator group. **Events:** Broadcasts `CollaboratorGroupResolverRegistryInitiated` so registered listeners can contribute additional field resolvers before the response is built. ## Get Conversion Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/get-by-id Retrieve a single conversion by ID with optional field selection. ### Get Conversion `GET /siren/v1/conversions/{id}` Returns a single conversion by ID. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | #### Example Request ``` GET /siren/v1/conversions/42?fields=id,status,engagementScore,programId ``` #### Example Response ```json { "id": 42, "status": "pending", "engagementScore": 100, "programId": 3 } ``` #### Error Responses Returns `404` if no record exists with that ID, or `500` on a database error. ## Get Conversion Types Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/conversion-types Retrieve the list of registered conversion types and their supported incentive structures. ### Get Conversion Types `GET /siren/v1/conversions/types` Returns the list of registered conversion types. Response is transformed by `RegistryResponseInterceptor`. #### Example Response ```json { "options": [ { "id": "sale", "name": "Sale" }, { "id": "lead", "name": "Lead" }, { "id": "renewal", "name": "Renewal" } ] } ``` ## Get Currency Types Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/currency-types Returns the list of available currencies that programs can use for reward denominations. # Get Currency Types `GET /siren/v1/programs/currency-types` Returns the list of available currencies that programs can use for reward denominations. **Example Response:** ```json [ { "id": "USD", "name": "US Dollar", "symbol": "$", "precision": 2 } ] ``` ## Get Distribution Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributions/get-by-id Returns a single distribution by ID, with optional extended fields for allocations. # Get Distribution `GET /siren/v1/distributions/{id}` Returns a single distribution by ID. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | **Example Request:** ``` GET /siren/v1/distributions/18?fields=id,status,value,allocations,allocationCount ``` **Example Response:** ```json { "id": 18, "status": "pending", "value": 5000, "allocations": [ { "distributionId": 18, "obligationId": 44, "status": "pending" }, { "distributionId": 18, "obligationId": 45, "status": "pending" } ], "allocationCount": 2 } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Get Distributor Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors/get-by-id Returns a single distributor by ID, including specialized domain fields for schedule, engagement types, and current distribution. # Get Distributor `GET /siren/v1/distributors/{id}` Returns a single distributor by ID, including specialized domain fields that are not available on the list endpoint. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields plus `currentDistribution` | **Example Request:** ``` GET /siren/v1/distributors/1?include=extended ``` **Example Response:** ```json { "id": 1, "name": "Standard Commission", "description": "Percentage-based commission on all sales", "distributionResolver": "percentage_based", "distributionPoolResolver": "standard_pool", "status": "active", "units": "USD", "dateCreated": "2026-01-15T10:00:00Z", "dateModified": "2026-03-20T14:30:00Z", "collaboratorCount": 12, "schedule": ["first day of next month"], "engagementTypes": [ { "id": 5, "type": "sale", "value": "100" } ], "transactionCompilers": ["standard"], "revenuePercentage": 30, "lineItemFilters": { "withTypes": ["products"], "collaboratorOwned": false, "withSkus": [], "inCategories": null }, "currentDistribution": { "id": 42, "distributorId": 1, "status": "pending" } } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Get Engagement Source: https://www.sirenaffiliates.com/documentation/resource-reference/engagements/get-by-id Retrieve a single engagement by ID with optional field selection. ### Get Engagement `GET /siren/v1/engagements/{id}` Returns a single engagement by ID. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | **Example Request:** ``` GET /siren/v1/engagements/91?fields=id,status,score,programName,collaboratorName ``` **Example Response:** ```json { "id": 91, "status": "active", "score": 100, "programName": "Affiliate Program", "collaboratorName": "Jane Smith" } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Get Engagement Types Source: https://www.sirenaffiliates.com/documentation/resource-reference/engagements/engagement-types Retrieve the list of registered engagement trigger types. ### Get Engagement Types `GET /siren/v1/engagements/types` Returns the list of registered engagement trigger types. Response is transformed by `RegistryResponseInterceptor`. **Example Response:** ```json { "options": [ { "id": "referredSiteVisit", "name": "Site Visited" }, { "id": "manual", "name": "Manual Attribution" }, { "id": "boundCouponUsed", "name": "Coupon code used" } ] } ``` The available types depend on which integrations are active. `referredSiteVisit` and `manual` are always present. `boundCouponUsed` only appears when a coupon-supporting e-commerce integration (WooCommerce, EDD, LifterLMS, or NorthCommerce) is active. ## Get Fulfillment Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments/get-by-id Retrieves a single fulfillment by ID with optional field selection. ### Get Fulfillment `GET /siren/v1/fulfillments/{id}` Returns a single fulfillment by ID. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | #### Example ``` GET /siren/v1/fulfillments/5?fields=id,status,payoutCount,totalValue,payouts ``` ```json { "id": 5, "status": "pending", "payoutCount": 2, "totalValue": 5000, "payouts": [ { "id": 20, "collaboratorId": 3, "value": 3000, "status": "unpaid" }, { "id": 21, "collaboratorId": 7, "value": 2000, "status": "unpaid" } ] } ``` Returns `404` if no record exists with that ID, or `500` on a database error. ## Get Incentive Types Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/incentive-types Returns the list of registered incentive resolver types available for programs. # Get Incentive Types `GET /siren/v1/programs/incentive-types` Returns the list of registered incentive resolver types available for programs. **Example Response:** ```json { "options": [ { "id": "percentage", "name": "Percentage of Sale" }, { "id": "flat_rate", "name": "Flat Rate per Conversion" } ] } ``` ## Get Note Position Source: https://www.sirenaffiliates.com/documentation/resource-reference/notes/position Look up which page of a source's activity feed a specific note sits on. ### Get Note Position `GET /siren/v1/notes/{id}/position` Returns the offset and total count for a specific note within a source's unfiltered activity feed. Use this when you have a note ID (for example, from a deep link or a notification) and need to jump to the page of the feed that contains it. **Path Parameters:** | Parameter | Type | Description | |---|---|---| | `id` | integer | The note ID | **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `sourceType` | string | _required_ | The source to compute the position against. | | `sourceId` | integer | _required_ | The source ID. | | `order` | string | `desc` | Sort direction the caller is using. Must match the order used in the corresponding list request. | **Example Request:** ``` GET /siren/v1/notes/904/position?sourceType=conversion&sourceId=118&order=desc ``` **Example Response:** ```json { "offset": 0, "total": 7 } ``` Pair this with a list request using the same `number` per page to calculate which page of the feed to load: ``` page = floor(offset / number) ``` ## Get Obligation Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations/get-by-id Retrieves a single obligation by ID with optional field selection. ### Get Obligation `GET /siren/v1/obligations/{id}` Returns a single obligation by ID with optional field selection. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | #### Example Request ``` GET /siren/v1/obligations/10?fields=id,status,value,collaboratorName,programName ``` #### Example Response ```json { "id": 10, "status": "pending", "value": 1500, "collaboratorName": "Jane Doe", "programName": "Standard Affiliate Program" } ``` #### Error Responses A `404` is returned if no record exists with the given ID. A `500` indicates a database error. ## Get Payout Source: https://www.sirenaffiliates.com/documentation/resource-reference/payouts/get-by-id Retrieves a single payout by ID with optional field selection. ### Get Payout `GET /siren/v1/payouts/{id}` Returns a single payout by ID. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | #### Example ``` GET /siren/v1/payouts/20?fields=id,value,currency,status,collaboratorName,collaboratorEmail ``` ```json { "id": 20, "value": 3000, "currency": "USD", "status": "unpaid", "collaboratorName": "Jane Doe", "collaboratorEmail": "jane@example.com" } ``` Returns `404` if no record exists with that ID, or `500` on a database error. ## Get Program Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/get-by-id Returns a single program by ID, with optional extended fields for engagement types and incentive configuration. # Get Program `GET /siren/v1/programs/{id}` Returns a single program by ID. When no `fields` parameter is provided, returns the default set: `id`, `name`, `description`, `status`, `units`, `incentiveType`, `incentiveResolverType`, `createdDate`, and `modifiedDate`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | **Example Request:** ``` GET /siren/v1/programs/1?fields=id,name,status,engagementTypes,incentiveCalculation ``` **Example Response:** ```json { "id": 1, "name": "Standard Affiliate Program", "status": "active", "engagementTypes": [ { "id": 5, "type": "link_click", "value": "100" } ], "incentiveCalculation": { "transactionPercent": "10", "payoutPerTransaction": null, "payoutPerProduct": null, "payoutPerLead": null, "activeConversionTypes": ["sale"], "autoApprove": "1", "leadRequalificationDays": null } } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. **Events:** Broadcasts `ProgramActionEvent` (action: Read) after success. ## Get Program Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups/get-by-id Returns a single program group by ID, with optional extended fields for associated program IDs. # Get Program Group `GET /siren/v1/program-groups/{id}` Returns a single program group by ID. When no `fields` parameter is provided, returns all core fields (`id`, `name`, `description`, `sorter`). **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | **Example Request:** ``` GET /siren/v1/program-groups/1?fields=id,name,programIds ``` **Example Response:** ```json { "id": 1, "name": "Affiliate Tiers", "programIds": [3, 7, 12] } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Get Status Types: Collaborators Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/status-types Retrieve the list of available collaborator statuses and their display tones. ### Get Status Types `GET /siren/v1/collaborators/status-types` Returns the list of available collaborator statuses and their display tones. **Example Response:** ```json { "options": { "active": "Active", "inactive": "Inactive", "pending": "Pending", "rejected": "Rejected" }, "tones": { "active": "positive", "inactive": "negative", "pending": "warning", "rejected": "negative" } } ``` ## Get Status Types: Programs Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/status-types Returns the list of available program statuses with display labels and visual tone hints. # Get Status Types `GET /siren/v1/programs/status-types` Returns the list of available program statuses with display labels and visual tone hints. **Example Response:** ```json { "options": { "active": "Active", "inactive": "Inactive" }, "tones": { "active": "positive", "inactive": "negative" } } ``` ## Get Transaction Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions/get-by-id Returns a single transaction by ID with optional field selection. ### Get Transaction `GET /siren/v1/transactions/{id}` Returns a single transaction by ID. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` for all available fields | **Example Request:** ``` GET /siren/v1/transactions/101?fields=id,status,details,totalValue,currency ``` **Example Response:** ```json { "id": 101, "status": "complete", "details": [ { "id": 1, "transactionId": 101, "name": "Pro Plan", "description": "Annual subscription", "type": "credit", "value": 9999, "quantity": 1, "units": "USD" } ], "totalValue": 9999, "currency": "USD" } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Get Transaction Sources Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions/sources Returns the list of registered transaction sources based on active integrations. ### Get Transaction Sources `GET /siren/v1/transactions/sources` Returns the list of registered transaction sources based on active integrations. Response is transformed by `RegistryResponseInterceptor`. **Example Response:** ```json { "options": [ { "id": "wc_order", "name": "WooCommerce" }, { "id": "edd_payment", "name": "Easy Digital Downloads" } ] } ``` ## Headless Attribution Source: https://www.sirenaffiliates.com/documentation/headless/attribution Track a referred visit and pass the Siren opportunity ID between your frontend and a WordPress installation running Siren. import FlowchartSteps from "@/components/blocks/FlowchartSteps.astro"; # Headless Attribution In a headless Siren install, the frontend and the WordPress backend share *one* piece of state: the **opportunity ID** that links a visit to a collaborator. This page covers the canonical way to mint that ID when a referred visitor arrives, persist it across page loads, and send it back at conversion time. Everything here ships in the core Siren plugin on the Essentials tier and above. ## The flow ." }, { title: "Frontend reports the visit", description: "POST to the site-visited event endpoint with the collaborator alias." }, { title: "Siren returns the opportunity ID", description: "200 OK with the new ID in the X-Siren-OID response header." }, { title: "Frontend persists the ID", description: "Store in a first-party cookie or local storage for the attribution window." }, { title: "Visitor converts", description: "Frontend sends X-Siren-OID back as a request header on the conversion call." }, { title: "Pipeline runs", description: "Siren resolves the opportunity and runs the standard conversion pipeline." }, ]} /> End-to-end this is about *fifteen* lines of code in a typical frontend: ```js // On the landing page, when ?ref=ABC123 is present. // Re-POST whenever a ref is present so Siren's resolver can decide whether // to merge with an existing opportunity or create a new one. const ref = new URLSearchParams(location.search).get("ref"); if (ref) { const existingOid = document.cookie.match(/siren_oid=([^;]+)/)?.[1]; const res = await fetch("/wp-json/siren/v1/event/site-visited", { method: "POST", headers: { "Content-Type": "application/json", ...(existingOid && { "X-Siren-OID": existingOid }), }, body: JSON.stringify({ collaboratorId: `tracking:${ref}` }), }); const oid = res.headers.get("X-Siren-OID"); // Match max-age to your program's attribution window // (/documentation/general/cookie-duration). if (oid) document.cookie = `siren_oid=${oid}; path=/; max-age=2592000; Secure; SameSite=Lax`; } // On the conversion call (checkout, form submit, etc.). // Omit the header entirely when there's no OID rather than sending an empty string. const oid = document.cookie.match(/siren_oid=([^;]+)/)?.[1]; fetch("/api/checkout", { method: "POST", headers: { ...(oid && { "X-Siren-OID": oid }) }, body: payload, }); ``` The cookie can't be `HttpOnly`, because the conversion-time fetch needs to *read* it. Keep `Secure` and `SameSite=Lax` in place so the cookie isn't transmitted over HTTP and is sent on top-level navigations only. That's the whole shape. The rest of this page is what each step *actually* does. ## Step 1: Track the visit The frontend posts the visit to the [`/event/site-visited`](/documentation/resource-reference/events/site-visited) endpoint, passing the collaborator's tracking alias as `collaboratorId`. The `tracking:` prefix tells Siren the value is a referral code, not an internal ID. That means you can read `?ref=` straight off the URL and forward it without a reverse lookup. The same [alias system](/documentation/resource-reference/aliases) handles this on WordPress-rendered sites. The opportunity ID comes back in the `X-Siren-OID` response header. Read it client-side and persist it for the next steps. ## Step 2: Persist the opportunity ID The opportunity ID needs to survive across requests and page loads, but it doesn't need to live forever. Siren's standard [attribution window](/documentation/general/cookie-duration) decides how long it remains valid. Once that window closes, the opportunity expires *regardless* of what your frontend does. The recommended pattern is a first-party cookie scoped to your frontend's domain, with a lifetime that matches your intended attribution window. Local storage works, but it loses the opportunity if the visitor clears site data or switches browsers. Reach for it only if the cookie path is closed off. If a returning visitor arrives with a new referral parameter, POST another `site-visited` event with the new `collaboratorId` and let Siren's resolver decide whether to merge or create a new opportunity. If you already have an opportunity ID stored, include it as `X-Siren-OID` on the inbound POST so Siren uses it as the resolution starting point. ## Step 3: Send the opportunity ID at conversion time When the visitor takes a conversion action (checking out, submitting a form, or anything else your program attributes on), your frontend forwards the request to WordPress with the persisted opportunity ID in a custom header: ``` X-Siren-OID: 4218 ``` Siren's response interceptor reads the header, resolves the opportunity, and feeds it into the same [conversion pipeline](/documentation/resource-reference/pipeline-overview) that runs for native WordPress traffic. The interceptor also writes the resolved ID back to the response in the same header, so your frontend can refresh its stored value if Siren merged it with another opportunity. What that looks like in practice depends on what triggers the conversion. Three paths cover the common cases: - **WooCommerce.** The Siren extension fires `SaleTriggered` itself and picks up the header along with the rest of the request. Attribution lands *automatically*. - **Direct conversion creates.** Posting to the [conversions endpoint](/documentation/resource-reference/conversions/create) requires admin auth, so this has to come from a server-side runtime. - **Non-WordPress commerce stack.** Reporting through [`/event/sale`](/documentation/resource-reference/events/sale) resolves the same header the same way. The path into Siren changes. The header contract is *constant*. ### Trust boundary worth understanding `X-Siren-OID` is set by the browser on every conversion call. At the network boundary it's *attacker-controlled*: anyone can pick an opportunity ID and try to credit a sale to it. The protection isn't cryptographic verification of the OID itself. Two different things bound the abuse vector: - The [conversions endpoint](/documentation/resource-reference/conversions/create) requires admin auth, so a stranger can't manufacture conversions directly through it. - Public sale events (`/event/sale`) get crediting decisions only when the rest of the payload represents *real* commerce: a real WooCommerce order, a real Stripe charge, etc. An attacker who can fabricate that has bigger problems than headless attribution. Where this matters most is `/event/site-visited`. It's unauthenticated by default and not rate-limited unless the install ships its own [`EventIngestionRateLimitMiddleware`](/documentation/extensions/quickstart). A scripted caller can iterate referral codes and inflate visit counts. Sites running real money through the public path should plan for rate limiting, bot protection, or fraud review. The plugin gives you the extension point. The abuse posture is the install's choice. ### What failure looks like Three failures show up on a first build: - **Unresolvable alias.** `POST /event/site-visited` with `collaboratorId: "tracking:DOES_NOT_EXIST"` falls through the alias resolver unchanged, then trips the validation middleware (collaborator ID must be numeric or resolve to one) and returns a 4xx with the error in the body. Treat this as "no attribution" rather than retrying. - **Stale OID at conversion time.** If the cookie holds an OID whose attribution window has closed, the conversion still goes through but lands without attribution. The response header in that case carries no replacement value. Surface this in your logs to detect window misconfigurations. - **CORS preflight refused.** `OPTIONS` requests must be allowed and `X-Siren-OID` must appear in `Access-Control-Allow-Headers`. Both are covered in the [introduction's CORS section](/documentation/headless/introduction#cross-origin-requests). The Siren marketing site you're reading right now uses this exact pattern. See [the REST API launch post](/blog/the-siren-rest-api-is-complete) for the broader story. ## Headless Collaborator Applications Source: https://www.sirenaffiliates.com/documentation/headless/collaborator-applications Letting people apply to become collaborators from a form on your headless frontend. Either ride your forms plugin's existing integration, or use Siren's signed JWT flow directly. import FlowchartSteps from "@/components/blocks/FlowchartSteps.astro"; # Headless Collaborator Applications A collaborator application form is the *one* place on a headless site where you might want a public form to write directly into Siren. Visitor fills out name and email, the frontend submits, a collaborator record gets created (or matched if the email is on file). [Set up a Program Registration Form](/documentation/getting-started/set-up-a-program-registration-form) covers the WordPress-rendered version. This page reframes it for headless. Two paths, and which fits depends on whether you're already running a forms plugin. ## The shortcut: use a forms-plugin integration Several forms plugins ship with Siren integrations that handle collaborator signup internally. Check the [integration feature matrix](/documentation/general/integration-feature-matrix) for the current list. If your form lives in one of them, prefer this path: 1. Build the form in the forms plugin, configured with the collaborator-signup integration. 2. From your headless frontend, POST responses to the forms plugin's REST endpoint. Each plugin documents its own submission API. 3. The plugin processes the submission, fires Siren's signup events, and the collaborator is created. Your frontend *never* touches Siren directly. The forms plugin is the seam. ## The DIY path: Siren's signed JWT flow The submission endpoint that creates collaborators is *public by design*. Anyone on the internet can POST to it. So what stops a stranger from signing themselves up to your highest-paying program with `statusOnSignup: active` and walking away with a referral code? A signed JWT. Your trusted server-side code mints a token naming which programs the applicant can join. The public submit endpoint refuses any request without one. The JWT *is* the trust boundary: signed with a server-only secret, scoped to the programs you chose, attached to every submission as proof the form was legitimate. > **Replay risk to design around.** The JWT does not currently set an expiry (`exp`) or single-use (`jti`) claim. A leaked token can be replayed until the signing secret rotates. Mint a fresh JWT *per render*, never cache one across visitors, and read the next section before deciding where the token lives. ### How the round trip works The mint endpoint is [Create Signup Form JWT](/documentation/resource-reference/collaborators/signup-form-jwt). It requires admin auth using a [WordPress application password](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/#authentication-plugins) from a server-side runtime. Three fields control what the token can do: - `programIds`: which [programs](/documentation/general/what-are-programs) the collaborator gets bound to. - `statusOnSignup`: `pending` (review required before engagements fire) or `active` (auto-approve and start crediting immediately). See [collaborator status types](/documentation/resource-reference/collaborators/status-types) for the full lifecycle. - `approveExisting`: if the email is on file, leave it alone or auto-approve it into the new program. The [WordPress-rendered registration form](/documentation/getting-started/set-up-a-program-registration-form) describes how this looks operationally. The submit endpoint is [Submit Collaborator Request](/documentation/resource-reference/collaborators/submit-request). Public, gated entirely by the JWT. A missing or signature-invalid JWT returns 400 before field validation runs. ### Where the token lives: direct vs proxied The JWT only does its job if it stays in *trusted* hands. Two ways to wire the form. The right one depends on your `statusOnSignup`: | `statusOnSignup` | Direct submission | Proxied submission | | ---------------- | --------------------------------------- | ------------------ | | `pending` | OK (worst case: review-queue spam) | OK | | `active` | **Avoid** (leaked token = auto-approve) | OK | **Direct submission** embeds the token in the rendered HTML and the browser POSTs straight to Siren. Least code. The token is *visible in page source*, replayable until the signing secret rotates. Fine for `pending`: a replay just queues another applicant for review. Throughput problem, not a fraud problem. **Proxied submission** sends the form to your own backend. Your backend validates input, mints a fresh JWT, and forwards server-to-server. The token *never reaches the client*. One extra hop, meaningfully better isolation. Required for `active`, where every leaked token is an auto-approved signup waiting to happen. In either pattern, mint per render and don't cache. **"Per render" only protects you when the page is rendered per-request.** On a statically generated frontend (Astro SSG, Next.js export), the form is built once and shipped to every visitor with the *same* token baked in. That's "one token forever," the worst case. If the form lives on a static page, use proxied submission so the token is minted at submit time, not build time. ## Headless Collaborator Groups Source: https://www.sirenaffiliates.com/documentation/headless/collaborator-groups Managing collaborator groups and binding programs and distributors to them over REST from a headless frontend. The binding is set with a collaboratorGroupId field on the program or distributor, not a separate endpoint. import FlowchartSteps from "@/components/blocks/FlowchartSteps.astro"; # Headless Collaborator Groups A [collaborator group](/documentation/general/what-are-collaborator-groups) is a named cluster of collaborators with a structure (flat, linear chain, or parent-child). Once a [program](/documentation/general/what-are-programs) or [distributor](/documentation/general/what-are-distributors) is bound to a group, the [upline cascade](/documentation/calculation-strategies/upline-cascade) and [downline cascade](/documentation/calculation-strategies/downline-cascade) calcs walk the group on every qualifying engagement. The admin UI manages all of this from the WordPress dashboard. When your provisioning lives in code, a headless onboarding flow, an external CRM sync, or an infrastructure-as-code setup, you do the same work over REST. This page covers the workflow and the auth framing. For the wire-level request and response shapes of every endpoint, see [Collaborator Groups (REST)](/documentation/resource-reference/collaborator-groups). ## Who this applies to This is a [fully headless](/documentation/headless/introduction#when-this-section-applies) concern. Group management is not referral tracking that you bolt onto a WordPress storefront. It is administrative provisioning, so it always runs server-side against the admin REST surface, regardless of how the rest of your site is wired. ## Authentication Every collaborator-group endpoint is an admin endpoint. None of them are public, unlike the collaborator submission endpoint covered on the [collaborator applications page](/documentation/headless/collaborator-applications). Call them from a server-side runtime (an Astro server function, a Next.js route handler, or a serverless function) using a [WordPress application password](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/#authentication-plugins). Treat that password as a server-side secret and never ship it to the browser. Capabilities matter here. Full create, read, update, and delete on the `CollaboratorGroup` resource is granted only to the `administrator` and `siren_platform_manager` roles. Mint the application password from a user in one of those roles. A token from a lower-privilege role will be rejected by the capability check before the handler runs. ## The two halves of the workflow Wiring a group into your payout structure is two separate jobs. Keep them straight, because they live at different endpoints. 1. **Manage the group itself.** Create the group, set its structure, and reconcile its members. All of this happens under the `/collaborator-groups` base. 2. **Bind a program or distributor to the group.** This does not happen under `/collaborator-groups` at all. You set a field on the program or distributor. The next section is the part most people miss. ## Binding is a field, not an endpoint A program or distributor does not store its group association on its own row, and there is no `/bind` endpoint. The binding lives in the shared `wp_siren_configs` table. You set it by including a `collaboratorGroupId` field in the body of a `PUT` (or `POST`) against the program or distributor itself: ``` PUT /programs/{id} ``` ```json { "collaboratorGroupId": 12 } ``` The distributor side is identical against its own resource: ``` PUT /distributors/{id} ``` ```json { "collaboratorGroupId": 12 } ``` A listener on the program or distributor action event writes the config row and broadcasts a `ProgramBoundToCollaboratorGroup` or `DistributorBoundToCollaboratorGroup` event. This runs synchronously inside the same save, so the binding is in place by the time the request returns, and a read-back of the program or distributor reflects it. Passing `null`, `""`, or `0` clears the binding, which deletes the config row (a cleared binding is indistinguishable from one that was never set) and broadcasts the matching `ProgramUnboundFromCollaboratorGroup` or `DistributorUnboundFromCollaboratorGroup` event. Each program or distributor can be bound to at most one group. The full config-row keying is in the [REST reference](/documentation/resource-reference/collaborator-groups#binding-a-program-or-distributor). ## A typical provisioning round trip You can fold step 1 and step 2 together. `POST /collaborator-groups` accepts a `members` array in the create body, so a group and its full roster can be written in one request. ## The ten endpoints at a glance There are ten endpoints across the group surface. Nine manage the group and its members, one reads the structure registry, and the binding reuses the program and distributor endpoints you already have. **Group lifecycle:** - `GET /collaborator-groups` lists groups. The `fields` query parameter is required. - `GET /collaborator-groups/{id}` reads one group. - `POST /collaborator-groups` creates a group, optionally with its initial members. - `PUT /collaborator-groups/{id}` updates `name`, `description`, or `structure`. Member changes do not go through this endpoint. - `DELETE /collaborator-groups/{id}` deletes the group and cascades its member rows. It does not detach bindings, so a program or distributor bound to the group keeps its `collaboratorGroupId` pointing at the now-deleted group, and its cascade then fails closed and credits no one. In a teardown flow, clear the binding first (a `PUT` against the program or distributor with `collaboratorGroupId` set to `null`), then delete the group. **Membership:** - `GET /collaborator-groups/{id}/members` lists the members of one group. - `POST /collaborator-groups/{id}/members` adds members. Idempotent, so collaborators already in the group are skipped. - `PUT /collaborator-groups/{id}/members` full-replaces the roster, reconciling the group against the payload. - `DELETE /collaborator-groups/{id}/members/{collaboratorId}` removes one member. **Structure registry:** - `GET /collaborator-groups/structures` lists the installed structure resolvers and the walker capabilities each one advertises. The structures endpoint is worth one note for headless callers. Its `providedWalkerCapabilities` field is what the admin calc picker reads to hide calc strategies a bound group cannot support. The cascade calcs need the `hasLayer` capability, which `linearChain` and `parentChild` provide and `flat` does not. That filter is a frontend convenience. The REST API does not enforce it, so a client that sets a cascade calc against a flat-bound program will persist successfully and then emit no credits at runtime. Since the API does not validate it, a headless flow should check this itself: before binding a program that uses a cascade calc, read `GET /collaborator-groups/structures`, find the group's structure, and confirm its `providedWalkerCapabilities` include `hasLayer`. The same hazard applies after the fact, because a `PUT /collaborator-groups/{id}` that switches a bound group to `flat` strips the capability and leaves the program's saved cascade crediting no one. See [Why some calculation methods disappear](/documentation/general/calc-capability-matching) for the picker behavior. ## Changing structure after the fact `PUT /collaborator-groups/{id}` can change a group's `structure`, and it broadcasts `CollaboratorGroupStructureChanged` when it does. Per-member metadata is not migrated when the structure changes. Switching a flat group to `linearChain` leaves every member without a `position`, which the linear-chain resolver treats as position 0. If your provisioning flips a group's structure, follow it with a `PUT /collaborator-groups/{id}/members` that supplies the metadata the new structure needs. ## See also - [Collaborator Groups (REST)](/documentation/resource-reference/collaborator-groups): the wire shapes, field tables, and full request and response bodies for every endpoint on this page. - [What are collaborator groups](/documentation/general/what-are-collaborator-groups): the operator-facing concept page. - [Choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure): when to use `flat`, `linearChain`, or `parentChild`. - [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group): the same surface from the admin UI. ## Headless Coupons Source: https://www.sirenaffiliates.com/documentation/headless/coupons How coupon-based attribution works on a headless site. The hybrid path is straightforward, but fully-headless commerce has gaps worth knowing about up front. # Headless Coupons Whether headless coupons need any code from you depends on one question: does WordPress still run the transaction? If a supported commerce plugin (WooCommerce, EDD, North Commerce, LifterLMS) is on the install, the answer is yes and you have *nothing* to do beyond passing the coupon through. If checkout has moved off WordPress entirely, you've stepped outside Siren's public surface for coupons and the rest of this page is about which workaround to pick. See [coupon code tracking](/documentation/general/coupon-tracking) for the full per-integration breakdown of how coupon attribution works inside Siren. ## Hybrid: WordPress commerce plugin still runs the transaction If your install runs a [supported commerce extension](/documentation/general/integration-feature-matrix) and checkout still happens server-side through it, do nothing special. The extension fires [`CouponApplied`](/documentation/developer-reference/events-commerce/coupon-applied) when the customer enters the code, and Siren's `BoundCouponUsed` engagement trigger handles the rest. The only requirement on the frontend is making sure the coupon code reaches the WordPress checkout, typically inside the cart payload. Don't apply the discount entirely on your own and skip the WordPress side. That bypasses the hook the integration depends on. ## Fully headless: no WordPress commerce plugin If checkout happens in your frontend or an external system with no WooCommerce or EDD on the install, coupon attribution gets harder. The public surface for it is missing. The event endpoint exposes only `site-visited`, `sale`, and `refund`. There's no `coupon-applied` slug. `CouponApplied` is broadcast internally by commerce extensions and has no public REST entry point. No event, no engagement. Three options, in order of preference. ### Option 1: Keep WooCommerce as the transaction layer Keep WooCommerce installed and route the transaction through it from your frontend via the WooCommerce REST API. The coupon hook fires, Siren picks up the rest, and you write *no* custom code. The storefront stays fully headless because WordPress is still doing the commerce work in the background. ### Option 2: Ship a small Siren extension that registers the event If routing through WooCommerce isn't an option, ship a Siren extension that registers a `CouponApplied` event factory. The extension declares a slug, parses the coupon code and opportunity ID out of the request body, and builds the event. Siren's pipeline takes over from there. The [extension quickstart](/documentation/extensions/quickstart) and [event bindings and transformers](/documentation/extensions/event-bindings-and-transformers) cover the patterns. This means shipping PHP onto the WordPress install, so it's only worth doing when you *genuinely* cannot run a commerce extension. ### Option 3 (last resort): POST conversions directly The escape hatch is to skip the event system and call the [conversions create endpoint](/documentation/resource-reference/conversions/create) directly from your frontend's server runtime, passing the opportunity ID in the same `X-Siren-OID` header used everywhere else. The endpoint requires admin auth, so it has to come from a server runtime holding a WordPress application password. The browser cannot call it. This works, but you give up real things the event-driven path produces for free: - The activity-feed note that normally records "coupon CODE redeemed on opportunity #N" comes from `CouponApplied`. Direct conversion creates skip it, so support staff lose the audit trail. - Without `BoundCouponUsed`, Siren can't resolve which collaborator owns a coupon. You keep that mapping outside the [alias system](/documentation/resource-reference/aliases) and resolve it before posting. - `RefundTriggered` normally cancels the matching conversion and subtracts value from collaborator metrics. Without an extension behind the sale, refunds are your job. POST [`/event/refund`](/documentation/resource-reference/events/refund) yourself or the credit stands. Reach for this path only when the other two are genuinely closed off, and document the workarounds so the gaps don't catch a teammate by surprise later. ## Headless Development Source: https://www.sirenaffiliates.com/documentation/headless/introduction Building on top of Siren when your frontend doesn't render through WordPress. Patterns for attribution, coupons, and collaborator applications on a headless install. # Headless Development A headless Siren install runs the Siren plugin on WordPress, but the user-facing frontend lives somewhere else. That frontend can be Astro, Next.js, Remix, a custom React app, a Shopify storefront, or anything else that talks to WordPress over HTTP. Siren still owns affiliate logic, attribution, programs, and payouts. The frontend owns the customer experience. This section is a developer's guide for the patterns that come up when you build that way. The endpoints are the same ones a WordPress-rendered site uses. What changes is that *your frontend code* is the caller. ## Vocabulary Opportunity, conversion, engagement, alias, program. If those terms aren't already familiar, [Vocabulary Translation](/documentation/general/vocabulary-translation) is the right starting point. It maps Siren's vocabulary to the terms used in other affiliate platforms and to non-affiliate use cases like LMS, marketplace, and SaaS. ## When this section applies Headless integrations come in two shapes. Each task page below calls out which one applies. **Hybrid** is when a headless frontend sits in front of a WordPress install that still runs WooCommerce, EDD, Gravity Forms, or another commerce or forms plugin. Transactions and form submissions still happen server-side through those plugins. The only thing you build on the frontend is referral tracking. **Fully headless** is when checkout, form submission, or data capture moves into your frontend or an external system. There's no WordPress commerce extension carrying the weight. The patterns get more involved, because Siren's hooks live inside those extensions and you're now operating outside them. ## Tier requirement Headless integration depends on the [event ingestion endpoints](/documentation/resource-reference/events), which require Essentials or above. Lite installs cannot serve as a backend for a headless frontend. ## Cross-origin requests Browser-side calls into Siren are subject to CORS when your frontend is on a different origin from the WordPress install. Siren's response interceptor sets `Access-Control-Expose-Headers: X-Siren-OID` automatically so JavaScript can read the opportunity ID across origins. `Access-Control-Allow-Origin` is left to WordPress. If you don't already have a CORS policy in place, the simplest setup is a `mu-plugin` that hooks `rest_pre_serve_request`. Scope it to Siren's namespace so it doesn't leak permissions to the rest of the WordPress REST API: ```php add_action('rest_pre_serve_request', function ($value) { if (strpos($_SERVER['REQUEST_URI'] ?? '', '/wp-json/siren/v1/') === false) { return $value; } header('Access-Control-Allow-Origin: https://your-frontend.example'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type, X-Siren-OID, Authorization'); return $value; }); ``` Two things to avoid. Don't set `Access-Control-Allow-Origin: *`. It weakens defenses against any future endpoint that depends on the browser origin. And don't pair a permissive origin with `Access-Control-Allow-Credentials: true`, because the combination opens credentialed cross-origin requests from any site. A reverse-proxy header or a CORS plugin works equally well as the `mu-plugin` route, but the same scoping and `*`-avoidance rules apply. ## Authentication Siren defers to WordPress's own REST authentication chain. Two patterns cover everything in this section. Admin endpoints are called from server-side runtimes using [WordPress application passwords](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/#authentication-plugins). The admin endpoints in this section are the JWT mint endpoint (collaborator-applications page), the conversions create endpoint (coupons page), and the collaborator group endpoints (collaborator groups page), which require the appropriate capability on the `CollaboratorGroup` resource. Server-side runtimes means Astro server functions, Next.js route handlers, or serverless functions. Treat the password as a server-side secret. Public endpoints, including the event ingestion endpoint and the collaborator submission endpoint, accept unauthenticated requests. Their protection comes from elsewhere. The collaborator submission endpoint is gated by the signed JWT covered on its own page. The event endpoints rely on whatever rate-limiting and bot-protection layers the install runs in front of them. Siren's plugin ships extension points (`EventIngestionAuthMiddleware`, `EventIngestionRateLimitMiddleware`) for installers who want to tighten that. ## What's in this section - [Attribution](/documentation/headless/attribution). Track a referred visit and persist the opportunity ID across page loads, then send it back at conversion time. The first thing you build for any headless setup. - [Coupons](/documentation/headless/coupons). Make sure coupon-based attribution still works when checkout doesn't render through WordPress. Mostly a matter of letting the WordPress commerce plugin do its job, with a note on what's harder if you went fully headless on commerce too. - [Collaborator applications](/documentation/headless/collaborator-applications). Let people apply to become collaborators from a form on your headless frontend. Either ride your forms plugin's existing integration or use Siren's signed JWT flow directly. - [Collaborator groups](/documentation/headless/collaborator-groups). Manage group rosters and bind programs or distributors to a group over REST. The binding is set with a `collaboratorGroupId` field on the program or distributor, not a separate endpoint. Requires Plus. ## Hooks, Actions & Events Source: https://www.sirenaffiliates.com/documentation/extensions/wp-hooks-and-events How add_action and do_action map to Siren's typed event system, including listening, broadcasting, and key differences. import CodeTabs from "@/components/content/CodeTabs.astro"; # Hooks, Actions & Events WordPress's hook system (`add_action`, `do_action`, `add_filter`, `apply_filters`) is the backbone of plugin development. Siren has an equivalent [event system](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners) built on typed PHP classes instead of string-named hooks. The concepts map directly: attaching a callback to a hook becomes attaching a handler to an event class, and firing a hook becomes broadcasting an event instance. ## Listening to events In WordPress, you listen to a hook by passing a string name and a callback. In Siren, you listen to a typed event class. There are two ways to do this: the facade approach for quick one-off attachments, and the DI approach for extension classes. ```php // WordPress: string-named hook, untyped callback add_action('woocommerce_order_status_completed', function ($order_id) { // $order_id is an int, but nothing enforces that error_log("Order completed: $order_id"); }); ``` ```php // Siren facade: typed event class, typed callback use Siren\Events\Core\Facades\Event; use Siren\Commerce\Events\SaleTriggered; Event::attach(SaleTriggered::class, function (SaleTriggered $event) { // $event is a typed object — IDE autocomplete works here $opportunityId = $event->getOpportunityId(); $details = $event->getTransactionDetails(); }); ``` ```php // Siren DI: handler class + initializer registration // 1. The handler class use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; /** * @implements CanHandle<\Siren\Commerce\Events\SaleTriggered> */ class LogSaleHandler implements CanHandle { public function handle(Event $event): void { // Your logic here } } // 2. Register in your Integration or Initializer use PHPNomad\Events\Interfaces\HasListeners; use Siren\Commerce\Events\SaleTriggered; class Integration implements Extension, HasListeners { public function getListeners(): array { return [ SaleTriggered::class => LogSaleHandler::class, ]; } } ``` The DI approach is preferred for extension development. Handler classes get full constructor injection, so any service the container knows about can be injected into your handler's constructor. The facade approach is useful for quick prototyping or code outside the DI context. ## Broadcasting events In WordPress, you fire a hook with `do_action` or `apply_filters`. In Siren, you broadcast a typed event instance. ```php // WordPress: fire a string-named hook with arbitrary arguments do_action('my_plugin_order_processed', $order_id, $customer_email); ``` ```php // Siren facade: broadcast a typed event instance use Siren\Events\Core\Facades\Event; Event::broadcast(new MyCustomEvent($orderId, $customerEmail)); ``` ```php // Siren DI: inject EventStrategy, broadcast from a service use PHPNomad\Events\Interfaces\EventStrategy; class OrderProcessingService { protected EventStrategy $events; public function __construct(EventStrategy $events) { $this->events = $events; } public function processOrder(int $orderId, string $email): void { // ... processing logic ... $this->events->broadcast(new MyCustomEvent($orderId, $email)); } } ``` ## How does apply_filters work in Siren? WordPress's `apply_filters` passes a value through a chain of callbacks, each modifying it and returning the result. Siren achieves the same thing through mutable events. Instead of passing a return value through a chain, you create an event instance with setters, broadcast it, and then read its state afterward. Listeners mutate the event during broadcast, so by the time `broadcast()` returns, the event carries the accumulated modifications. ```php // WordPress: pass a value through a chain of filter callbacks $price = apply_filters('my_plugin_price', $basePrice, $product); // Each callback receives the value, modifies it, and returns it add_filter('my_plugin_price', function ($price, $product) { if ($product->isPremium()) { return (int) ($price * 0.9); } return $price; }, 10, 2); ``` ```php // Siren facade: create a mutable event, broadcast it, read the result use Siren\Events\Core\Facades\Event; $event = new PriceCalculationRequested($basePrice, $product); Event::broadcast($event); $price = $event->getPrice(); // Listeners may have modified this // Attach a listener that mutates the event Event::attach(PriceCalculationRequested::class, function (PriceCalculationRequested $event) { if ($event->getProduct()->isPremium()) { $event->setPrice((int) ($event->getPrice() * 0.9)); } }); // The event class declares which fields are mutable via setters class PriceCalculationRequested implements \PHPNomad\Events\Interfaces\Event { protected int $price; protected Product $product; public function __construct(int $price, Product $product) { $this->price = $price; $this->product = $product; } public function getPrice(): int { return $this->price; } public function setPrice(int $price): void { $this->price = $price; } public function getProduct(): Product { return $this->product; } public static function getId(): string { return 'price_calculation_requested'; } } ``` ```php // Siren DI: broadcast from a service, handle in a listener class // In your service class PricingService { protected EventStrategy $events; public function __construct(EventStrategy $events) { $this->events = $events; } public function calculatePrice(int $basePrice, Product $product): int { $event = new PriceCalculationRequested($basePrice, $product); $this->events->broadcast($event); return $event->getPrice(); } } // In your handler class class ApplyPremiumDiscount implements CanHandle { public function handle(Event $event): void { if ($event instanceof PriceCalculationRequested && $event->getProduct()->isPremium()) { $event->setPrice((int) ($event->getPrice() * 0.9)); } } } ``` Siren uses this pattern internally for events like `TransactionCreateRequested` (where listeners can modify transaction details before they are persisted) and `CollaboratorSubmissionReceived` (where listeners can modify registration data before the account is created). If an event class has setters, it is designed to be mutated by listeners. ## Key differences from WordPress hooks WordPress hooks are identified by strings (`'save_post'`, `'the_content'`). Siren events are identified by PHP class names (`SaleTriggered::class`). Typos are caught at compile time, and your IDE provides autocomplete for event properties. `Event::attach()` accepts an optional `?int $priority` parameter, similar to WordPress's priority argument on `add_action`. Listeners registered through `getListeners()` run at default priority in registration order. Use `Event::detach()` to remove a previously attached listener, analogous to `remove_action` in WordPress. You can register multiple handler classes for the same event in `getListeners()` by passing an array: ```php public function getListeners(): array { return [ Ready::class => [ RegisterBlocks::class, EnqueueAdminAssets::class, SetupCollaboratorAdmin::class, ], ]; } ``` All three handlers fire when `Ready` is broadcast. ## Where to go next For the full handler pattern (constructor injection, the `CanHandle` interface, and common listener patterns), see [Listeners & Event Handlers](/documentation/extensions/listeners). For how WordPress hooks get translated into Siren domain events (the mechanism that makes integrations work), see [Bridging Platform Hooks](/documentation/extensions/wp-bridging-hooks). For the full PHPNomad framework documentation on event listeners, see [Event Listeners](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). ## How Refunds Work Source: https://www.sirenaffiliates.com/documentation/general/how-refunds-work What happens in Siren when a customer gets a refund: how conversions, obligations, and payouts are affected. import EventFlow from "@/components/content/EventFlow.astro"; # How Refunds Work When a customer gets a refund, Siren needs to unwind the affiliate commission that was created from that sale. This page explains what happens automatically and what, if anything, you need to handle yourself. ## What triggers a refund Siren does not process refunds directly. Instead, it listens for status changes in your connected commerce plugin (WooCommerce, LifterLMS, Easy Digital Downloads, or NorthCommerce). When an order moves to a refunded, cancelled, failed, or trashed status, the integration fires a `RefundTriggered` event and Siren begins reversing the records tied to that order. You do not need to tell Siren about a refund. As long as the refund happens inside your commerce plugin, Siren picks it up automatically. ## What happens to the conversion Every [conversion](/documentation/general/what-is-a-conversion) linked to the refunded order is set to "rejected." This is the same status a conversion gets when you reject it manually, but in this case the system handles it for you. A rejected conversion will no longer count toward collaborator performance. ## What happens to the obligation When a conversion is rejected, Siren checks the [obligation](/documentation/general/what-are-obligations) tied to it. If the obligation has not been paid out yet (its status is anything other than "complete"), Siren automatically sets it to "rejected." A rejected obligation will not be included in future [fulfillments](/documentation/general/what-is-a-fulfillment). If the obligation has already been marked as "complete" (meaning it was included in a fulfillment and a payout record already exists), Siren leaves it alone. Automatically reversing a payout that may have already been sent to a collaborator could create accounting problems, so the system does not do that on its own. ## What happens to distribution metrics If you use revenue-based [distributors](/documentation/general/what-are-distributors), Siren also subtracts the refunded transaction value from the current distribution period. This keeps your distribution pool accurate so that future payouts are calculated against the correct revenue totals. ## What happens if the payout already went out When a refund comes in after the obligation has been fulfilled, Siren will still reject the conversion and cancel the transaction, but it will not modify the completed obligation or the payout record. This is by design. At that point, money may have already changed hands, and the right course of action depends on your program's policies and your relationship with the collaborator. In this situation, you have a few options: - Deduct the amount from the collaborator's next payout manually. - Reach out to the collaborator and arrange a return of the overpayment. - Absorb the cost if the amount is small or if your program terms allow it. Siren does not automate any of these steps because each one involves a judgment call that depends on your specific circumstances. ## Partial refunds Siren currently treats refunds as all-or-nothing at the transaction level. When a commerce plugin signals a refund, Siren reverses the full conversion and obligation tied to that order. There is no built-in mechanism to reduce a commission by a partial amount. If you issue a partial refund through your commerce plugin and the order status does not change (for example, WooCommerce keeps the order as "completed" after a partial refund), Siren will not fire the refund pipeline at all. The conversion and obligation remain unchanged. If you need to handle a partial refund, you will need to reject the existing obligation and work out the adjusted amount outside of Siren's automated pipeline. ## Summary of the automatic pipeline Everything listed above happens without any action on your part. Each step also writes an entry to the [activity feed](/documentation/general/activity-feeds) on every record it touches, so you can open the refunded transaction, the rejected conversion, or the affected collaborator and see the full chain in order. This is the most useful way to verify that a refund actually reversed the things you expected it to reverse, especially if you're reconciling records for accounting. ## Timing payouts around your refund window The simplest way to avoid refund complications is to hold off on creating fulfillments until your refund window has passed. If your store offers a 30-day refund policy, waiting at least 30 days before paying out obligations gives the automatic pipeline time to catch any refunds and reject the affected obligations before you send money. This is not a requirement, but it is a practical safeguard. If you pay out an obligation and a refund comes in later, recovering that money becomes a manual process between you and the collaborator. ## When you need to step in The only scenario that requires manual attention is when a refund arrives after the obligation has already been fulfilled and paid out. In that case, review the affected payout and decide how to recover the commission based on your program's terms. For the technical details of the refund event and the listeners that handle it, see the [RefundTriggered event reference](/documentation/developer-reference/events-commerce/refund-triggered). > **For developers:** This concept maps to the [`RefundTriggered`](/documentation/developer-reference/events-commerce/refund-triggered) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## How to Control Which Program Gets Credit Source: https://www.sirenaffiliates.com/documentation/getting-started/multiple-affiliate-programs-using-sirens-program-groups Step-by-step instructions on how to set up multiple affiliate groups, which allow you to run multiple affiliate programs as if it's a single program. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; import StepList from "@/components/content/StepList.astro"; ## The double-payment problem When you run more than one [program](/documentation/general/what-are-programs), each one evaluates conversions independently. If a customer interacts with collaborators from two different programs before buying, both programs fire. Usually that's fine: an affiliate program and a royalty program serve different purposes and should both pay out. The issue is when two programs are variations of the same thing. A classic example: a standard affiliate program anyone can join, plus a super affiliate program with higher commissions for top performers. If a customer clicks a regular affiliate's link and later clicks a super affiliate's link, both programs fire on purchase and you pay two commissions for what should've been one referral credit. This is the double-payment problem, and [program groups](/documentation/general/what-are-program-groups) solve it. On Siren Lite there are no program groups. Build tiers there by [stacking programs](/documentation/getting-started/affiliate-tiers-on-lite-by-stacking-programs) instead: a base program at the lowest rate that every affiliate is in, plus a top-up program at the difference that only the higher tier is in, so one order never pays more than the top rate. A program group is not the same thing as a [collaborator group](/documentation/general/what-are-collaborator-groups). A program group decides which of several programs gets credit for one conversion. A collaborator group bundles people into a roster that a program or distributor binds to, and it is what powers tiered or sales-override payouts through [cascades](/documentation/general/what-is-a-cascade). If you want one sale to pay a collaborator's upline or downline, that is a collaborator group, not a program group. ## How program groups prevent double-paying A program group tells Siren to treat several programs as one for the purpose of deciding credit. Only one program in the group activates per [conversion](/documentation/general/what-is-a-conversion). The winning collaborator still earns the rate defined by their specific program, but they won't stack with or compete against other programs in the same group. To create one, click Program Groups in the sidebar and click Add New. Give it a name, description, group structure, and the programs that belong together. ## Picking a group structure The structure decides which program wins when multiple programs in the group have matching [engagements](/documentation/general/what-is-an-engagement) for a conversion. [Newest engagement wins](/documentation/program-group-structures/newest-engagement-wins) credits whichever collaborator interacted most recently. If a regular affiliate shared a link Monday and a super affiliate shared one Wednesday, the super affiliate wins a Thursday purchase. This matches typical "last click wins" attribution. [Oldest engagement wins](/documentation/program-group-structures/oldest-engagement-wins) credits whoever interacted first. This protects the original referrer's credit regardless of who engaged after. Not sure which to pick? See [Choosing a Program Group Structure](/documentation/program-group-structures/choosing-a-program-group-structure) for a full walkthrough. ## Group them or let them stack? Group programs together when they represent tiers or variations of the same incentive and only one should pay per conversion. The affiliate and super affiliate example is the classic case: both programs reward the same action at different rates. Leave programs ungrouped when they reward genuinely different contributions and should pay independently. A royalty program paying course creators when their content sells serves a different purpose than an affiliate program paying the referrer. Both should fire on the same transaction because they're compensating different people for different work. The rule of thumb: if two programs could realistically both apply to the same conversion and you'd be happy paying both, keep them separate. If you'd only ever want one to fire, group them. The video sets up the scenario: a public affiliate program anyone can join, plus a super affiliate program with higher commissions for top performers. The problem is that when a customer engages with both before converting, Siren fires both programs and you end up paying two commissions on a single sale. The fix is a program group. The walkthrough creates one called "Affiliate Programs" from Program Groups > Add New, gives it a description, and picks Newest Engagement Wins so the most recent engagement decides which program fires. Both the affiliate and super affiliate programs are added to the group. After saving, only one program in the group activates per conversion, while each still pays at its own rate. The takeaway: if two programs could both apply to the same conversion and you'd be happy paying both, keep them separate. Otherwise, group them. ## How to Pay Collaborators Source: https://www.sirenaffiliates.com/documentation/getting-started/how-to-pay-collaborators Step-by-step instructions on how to generate payouts, and manage obligations. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; import StepList from "@/components/content/StepList.astro"; ## How obligations accumulate Every [conversion](/documentation/general/what-is-a-conversion) Siren approves creates an [obligation](/documentation/general/what-are-obligations), a record of money you owe a specific [collaborator](/documentation/general/what-is-a-collaborator) for a specific transaction. They build up under Siren > Obligations as sales come in. Each entry shows the collaborator, amount, transaction, and status. Until you act, they sit as "pending." For the underlying concepts, see [What are obligations](/documentation/general/what-are-obligations) and [What is a fulfillment](/documentation/general/what-is-a-fulfillment). ## Create a fulfillment A [fulfillment](/documentation/general/what-is-a-fulfillment) takes pending obligations, groups them by collaborator, and produces one payout record per person. If Steve has three obligations for $5, $8, and $12, the fulfillment turns them into a single $25 payout for Steve. You can either select specific obligations with the checkboxes or click "Create fulfillment from pending" to scoop up everything at once. Either way you'll see a summary screen with the total, the number of obligations included, and the per-collaborator breakdown before you commit. Obligations", description: "Review the pending obligations from your programs." }, { title: "Select obligations or click \"Create fulfillment from pending\"", description: "Check specific obligations to include, or grab every pending obligation at once." }, { title: "Review the fulfillment summary", description: "Confirm the total, the number of obligations, and the per-collaborator breakdown." }, { title: "Click \"Generate fulfillments\"", description: "Obligations are marked complete and grouped into payout records, one per collaborator." }, ]} /> After generating, head to Siren > Fulfillments and click in to see the individual payouts. Each obligation now carries a payout ID, and you can click that ID from the obligations screen to filter and see exactly which obligations a payout covered. ## Pay people Siren tracks what you owe and organizes the records, but the actual money moves outside Siren through whatever method you use (PayPal, bank transfer, check, etc.). There are two ways to handle the handoff. Fulfillments and click into the fulfillment", description: "View the payouts generated for each collaborator." }, { title: "Pay individually: select a payout, click \"Mark as paid\", and click Apply", description: "Use this when sending payments one at a time." }, { title: "Pay in bulk: click \"Fulfill payouts\" and download the CSV", description: "The CSV has collaborator details and amounts. Import it into your payment processor. All payouts are automatically marked as paid." }, { title: "Mark a payout as unpaid if needed", description: "If a payment fails or a collaborator says they never received it, set the payout back to unpaid." }, ]} /> ## Undo and adjust You can flip a payout back to unpaid at any time. If a payment fails or a collaborator reports they never got it, just mark it unpaid and the record reflects reality. If a customer requests a refund, Siren handles obligation cleanup automatically. See [How Refunds Work](/documentation/general/how-refunds-work) for what happens to obligations and payouts when a transaction is reversed. The fulfillment system gives you a complete audit trail. Every obligation links to a payout, every payout links to a fulfillment, and statuses reflect the current state of each payment. That paper trail is useful for accounting, taxes, and resolving disputes. Pending obligations build up under Siren > Obligations as conversions happen. When you're ready to pay, you create a fulfillment, which groups obligations by collaborator and generates one payout per person. You can either hand-pick obligations with the checkboxes or click "Create fulfillment from pending" to bundle every pending obligation in one shot. Once the fulfillment exists, each payout can be marked paid one at a time, or you can click "Fulfill payouts" to export a CSV (handy for importing into your payment processor) and mark every payout in the fulfillment paid in bulk. If something goes wrong after the fact, like a failed payment or a collaborator saying they never received it, you can mark a payout unpaid at any time. Every obligation, payout, and fulfillment stays linked, so you've got a clean audit trail end-to-end. ## Integration Feature Matrix Source: https://www.sirenaffiliates.com/documentation/general/integration-feature-matrix Which WordPress integrations support which Siren features: conversion types, engagement triggers, coupon tracking, and more. Siren works with several WordPress plugins out of the box. Every integration ships with both the Lite and Essentials tiers, so you do not need to purchase anything extra to use them. As long as the required plugin is active on your WordPress site, Siren will detect it and enable its features automatically. The table below shows exactly which features each integration supports. ## Feature Comparison | Feature | WooCommerce | Easy Digital Downloads | LifterLMS | LearnDash | NorthCommerce | Gravity Forms | Ninja Forms | |---|---|---|---|---|---|---|---| | **Conversion Types** |||||||| | [Sale](/documentation/general/what-is-a-conversion) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | [Lead](/documentation/general/what-is-a-conversion) | -- | -- | -- | -- | -- | Yes | Yes | | [Renewal](/documentation/general/what-is-a-conversion) | With WooCommerce Subscriptions | With EDD Recurring Payments | Yes | -- | -- | -- | -- | | Refund handling | Yes | Yes | Yes | -- | Yes | -- | -- | | **Engagement Triggers** |||||||| | [Site visited](/documentation/general/site-visited) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | [Coupon code used](/documentation/general/coupon-code-used) | Yes | Yes | Yes | -- | Yes | -- | -- | | [Collaborator product sold](/documentation/general/collaborator-product-sold) | Yes | Yes | Yes | Yes | Yes | Yes | -- | | [Course completed](/documentation/general/course-completed) | -- | -- | Yes | Yes | -- | -- | -- | | [Lesson completed](/documentation/general/lesson-completed) | -- | -- | Yes | Yes | -- | -- | -- | | [Blog post visited](/documentation/general/blog-post-visited) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | | Form submitted | -- | -- | -- | -- | -- | Yes | Yes | | **Commerce Features** |||||||| | Coupon tracking | Yes | Yes | Yes | -- | Yes | -- | -- | | Product ownership (assign collaborators to products) | Yes | Yes | Yes | Yes | Yes | Yes | Form-level only | | Transaction line item details | Yes | Yes | Yes | Yes | Yes | Yes | -- | | Manual order creation | Yes | Yes | -- | -- | -- | -- | -- | A few notes on how to read this table: - "Site visited" and "Blog post visited" are handled by Siren's core WordPress integration, so they work regardless of which commerce plugin you use. - "Collaborator product sold" requires that a product (or course, or form product field) is assigned to a collaborator. The mechanism for assigning products varies by integration, but the concept is the same. - Renewal support in WooCommerce requires the WooCommerce Subscriptions plugin. In Easy Digital Downloads, it requires the Recurring Payments extension. - LifterLMS handles coupon detection differently from the other commerce plugins. Instead of a separate coupon event, it detects coupons when the sale is processed. - Ninja Forms assigns a collaborator at the form level, not to individual fields. It fires the Form submitted event and treats a form with a payment action as a sale, but it does not track per-product ownership or itemized line items. A payment taken through a Ninja Forms payment action is recorded as a single total. ## WooCommerce The WooCommerce integration connects Siren with the most widely used WordPress e-commerce plugin. It tracks sales, coupons, refunds, and subscription renewals. You can assign collaborators to individual products through a dedicated tab on the product edit screen. Requires the [WooCommerce](https://woocommerce.com/) plugin. ## Easy Digital Downloads The Easy Digital Downloads integration is built for stores that sell digital products like software, PDFs, music, and other downloadable files. It supports the full commerce lifecycle including sales, refunds, coupons, and subscription renewals. You can assign collaborators to downloads through a metabox on the download edit page. Requires the [Easy Digital Downloads](https://easydigitaldownloads.com/) plugin. ## LifterLMS The LifterLMS integration bridges course sales with affiliate tracking. Beyond standard commerce features, it can fire engagement triggers when students complete courses or individual lessons, which makes it a good fit for instructor royalty programs. Course instructors are automatically linked as collaborators when courses are saved. Requires the [LifterLMS](https://lifterlms.com/) plugin. ## LearnDash The LearnDash integration tracks course purchases and student progress. It supports sale tracking and fires engagement triggers when students finish courses or lessons. Course authors are automatically mapped to collaborators, enabling instructor-based royalty programs. It does not currently support coupon tracking or refunds. Requires the [LearnDash](https://www.learndash.com/) plugin. ## NorthCommerce The NorthCommerce integration works with the lightweight NorthCommerce e-commerce plugin. It supports sale tracking, coupon detection, and refund handling. You can assign collaborators to products through the NorthCommerce product editor. Requires the NorthCommerce plugin and Siren version 1.3 or later. ## Gravity Forms The Gravity Forms integration turns form submissions into trackable events. Forms with product fields are treated as sales, while forms without product fields are treated as leads. You can assign collaborators to individual product fields within the Gravity Forms editor, and Siren merge tags are available for use in form notifications. Requires the [Gravity Forms](https://www.gravityforms.com/) plugin. ## Ninja Forms The Ninja Forms integration turns form submissions into trackable events. A form with a payment action is treated as a sale, recorded as a single payment total, while a form without one is treated as a lead. You assign a collaborator to a form rather than to individual fields, and Siren merge tags are available for use in form actions and notifications. It does not track itemized line items or per-product ownership. Requires the [Ninja Forms](https://ninjaforms.com/) plugin. ## Investigating a Collaborator Dispute Source: https://www.sirenaffiliates.com/documentation/getting-started/collaborator-disputes What to do when a collaborator says they sent a customer who didn't get attributed. A workflow for tracing opportunities, engagements, and conversions back through the pipeline. Once in a while, a collaborator will tell you they sent a customer who didn't get attributed. Sometimes they're right and something broke. Sometimes they're not and the customer never actually clicked the link. Either way, you need a way to figure out what happened. This page walks through how to investigate. ## The basic shape of an investigation Siren's attribution pipeline has three stages, and a dispute almost always comes down to one of them failing. You need to answer three questions in order: 1. Did the visit ever reach Siren? Look for an [opportunity](/documentation/general/what-is-an-opportunity). 2. If it did, was it tied to the collaborator? Look for an [engagement](/documentation/general/what-is-an-engagement). 3. If it was, why didn't it produce a conversion? Look at conversion creation, engagement expiration, program configuration, and self-referral filtering. Answering these in order tells you whether the problem is tracking, attribution, or program rules. It also tells you whether the collaborator's claim holds up. ## Step 1: check for an opportunity Open the Opportunities screen in the Siren admin. Search for the visitor by user ID if they were logged in, by email if you have it, or by approximate timestamp if those don't match. You're looking for any opportunity that corresponds to this visitor's session around the time they claim to have clicked the link. If there's no opportunity at all, the visit never reached Siren. The tracking script didn't fire, or it did fire but the visitor's browser blocked the cookie before anything got recorded. Common causes: - The collaborator's link wasn't using a tracking ID parameter, so Siren had nothing to record. - The visitor blocked cookies or used a privacy-focused browser that dropped the pageview before Siren could store it. - The visit landed on a domain that doesn't have the Siren tracking script loaded (for example, a checkout subdomain that wasn't configured). In most of these cases, the collaborator's claim is essentially unverifiable. You can take their word for it or not, but Siren can't confirm or deny. ## Step 2: check for an engagement If an opportunity exists, check whether it has any engagements attached. The opportunity represents "Siren saw this visit," and the engagement represents "Siren tied this visit to a specific collaborator." An opportunity without engagements means Siren saw the visit but couldn't figure out who to credit. Common causes: - The tracking ID in the URL didn't match any collaborator's [alias](/documentation/resource-reference/aliases). A typo in the link, or a link built before the collaborator was assigned an alias. - The coupon code at checkout wasn't assigned to a collaborator. See [Coupon Code Tracking](/documentation/general/coupon-tracking) for how assignments work. - The visit happened before the collaborator was added to the program, so the engagement trigger had nothing to match against. - The engagement trigger the visit should have fired isn't enabled on the program at all. This stage is where most legitimate disputes land. The visit reached Siren, but a configuration problem prevented it from being credited. ## Step 3: check for a conversion If engagements exist but no conversion fired, you're looking at a program-level issue. The engagement was created, but something prevented it from turning into a commission. Things to check: - Did the engagement expire before the customer purchased? Engagements have a cookie window, and a purchase after that window won't produce a conversion. - Is the engagement type enabled in the program the conversion would have fired in? A program that only tracks coupon codes won't turn a site-visit engagement into a conversion. - Was the buyer logged in as a collaborator? See [Self-Referral Prevention](/documentation/general/self-referral-prevention). If they were, the opportunity was invalidated and no conversion could fire. - Is the program in a [program group](/documentation/general/what-are-program-groups) with a sorter that gave the credit to a different engagement? The dispute might be that the right engagement lost the sort. If you find an issue in any of these areas, you've diagnosed the problem and can decide how to resolve it. ## Resolving the dispute Once you know what happened, the resolution depends on which bucket you're in. If the system worked correctly and the collaborator has no claim, explain what you found. Siren's data is auditable, and walking the collaborator through the opportunity screen (or screenshotting it for them) is usually enough to close the conversation. The [activity feed](/documentation/general/activity-feeds) on the relevant conversion, obligation, or collaborator record is the most direct way to do this. It's a timestamped, system-authored sequence of every event Siren recorded for that record, so instead of reconstructing the story across three screens you point at one screen and say "here's what happened and when." A dispute that ends with "here's what we can both see on our side" is a lot easier than a dispute that ends with "we'll take your word for it." If the system worked but missed something legitimate (the customer ordered by phone after clicking the link, or the cookie expired a day before checkout), use [manual attribution](/documentation/getting-started/manually-attribute-a-transaction) to credit the collaborator after the fact. This runs the transaction through the normal pipeline, so the conversion and obligation get created cleanly. If the system was misconfigured (the engagement type wasn't enabled in the right program, the coupon wasn't assigned correctly, the alias was missing), fix the configuration first. Then decide whether to manually attribute the disputed transaction as a one-off. Usually you should, because the collaborator's claim is valid and the only reason it didn't get credited is that you didn't have Siren set up right. ## Preventing future disputes A few operational habits reduce the number of disputes you have to investigate in the first place. Hold conversions in pending status during the early days of a program. This gives you a chance to catch configuration problems before they turn into payout complaints. The same habit is the single biggest fraud protection you have, and [how to spot and prevent affiliate fraud](/blog/how-to-spot-and-prevent-affiliate-fraud) walks through the patterns worth watching for during that review. Watch for collaborators with high click counts and zero conversions. That pattern usually means the clicks are reaching Siren but something's breaking between engagement and conversion, which is exactly the kind of gap that generates disputes once collaborators notice their dashboard is empty. Regularly audit your most-active collaborators to confirm their alias entries are valid and their coupon codes are still assigned to them. A collaborator whose alias disappeared (because of an import, a manual edit, or a code reassignment) will stop earning commissions silently, and you'd rather catch that before they write you an angry email. ## LeadTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce/lead-triggered The non-monetary equivalent of SaleTriggered. Fires when a form submission or signup action occurs. # LeadTriggered `LeadTriggered` is the non-monetary equivalent of `SaleTriggered`. It fires when a form submission or signup action occurs, allowing Siren to track lead-based conversions through the same attribution pipeline that handles sales. Form-based integrations like Gravity Forms use this event instead of `SaleTriggered`. See [Form Submitted](/documentation/general/form-submitted) for a user-level overview of how form-based engagement tracking works. The event ID is `lead_triggered`, and its fully qualified class is `Siren\Commerce\Events\LeadTriggered`. ## What does this event carry? The event carries an opportunity ID and a source string, but no transaction details. Leads have no monetary value at trigger time. Like `SaleTriggered`, it also accepts optional binding fields to map the event back to an external record. ```php use Siren\Commerce\Events\LeadTriggered; $event = new LeadTriggered( $opportunityId, // int: the tracked referral 'gravity_forms', // string: source extension $formId, // ?string: external form ID 'gf_form' // ?string: external type ); ``` ## How does the pipeline react? `InitializeLeadConversion` picks up this event and begins the lead conversion process. The downstream pipeline creates conversions and obligations using lead-specific incentive types rather than sale-based ones. During initialization, the system fires a `LeadInitialized` event (event ID: `lead_initialized`), which parallels `SaleInitialized` in the sale flow. This is an internal coordination point for listeners that need to act during lead initialization before conversions are fully built. For the full lifecycle of commerce events and how they connect to the rest of the attribution pipeline, see the [Commerce Events overview](/documentation/developer-reference/events-commerce). ## Lesson Completed Source: https://www.sirenaffiliates.com/documentation/general/lesson-completed When a student completes a lesson in a course owned by a collaborator, the Lesson Completed event is triggered. import EventFlow from "@/components/content/EventFlow.astro"; When a student finishes a lesson inside a course owned by a [collaborator](/documentation/general/what-is-a-collaborator), the "Lesson Completed" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the progress to the instructor who owns the course. ## How it's typically used Lesson Completed is a finer-grained version of [Course Completed](/documentation/general/course-completed). Instead of waiting for students to finish an entire course before an instructor earns anything, this trigger rewards progress along the way. That's useful when courses are long, when students don't always reach the end, or when you want the instructor's earnings to reflect real engagement rather than just completion counts. Most sites combine the two triggers: a small reward per lesson, a larger reward for finishing the course. The [instructor revenue share](/recipes/instructor-revenue-share) recipe uses exactly that approach. ## Integration requirements Lesson Completed requires either the LifterLMS or LearnDash integration. Siren listens for lesson completion events from the LMS and attributes them to the collaborator who owns the parent course. ## Linear chain structure Source: https://www.sirenaffiliates.com/documentation/collaborator-group-structures/linear-chain An ordered chain of collaborators where each position has at most one upline and one downline. A linear chain is an ordered list of collaborators where every position has at most one upline (the position directly before it) and one downline (the position directly after it). It's the simplest shape that supports a [cascade](/documentation/general/what-is-a-cascade), and the right pick when your org chart is a ladder rather than a tree. ## How it works Every member of a linear chain carries an integer `position` in their metadata. Lower numbers sit closer to the top. Position 1 is the head of the chain. Position N is the tail. Positions don't have to be perfectly contiguous, but the natural shape (and what the admin UI produces) is `1, 2, 3, …` with no gaps and no duplicates. Treat positions as a strict order, not as a score. Layers are counted by rank in that order, not by the numeric gap between positions, so positions 1, 5, 9 behave exactly like 1, 2, 3 and yield layers 1, 2, 3. From any member's point of view, upline means walking toward lower positions, so the person at position 4 has position 3 as their layer-1 upline, then position 2 as layer 2, then position 1 as layer 3. Downline runs the other way, toward higher positions, so position 4 has position 5 as their layer-1 downline, position 6 as layer 2, and so on. Either direction is measured in [layers](/documentation/general/what-is-a-cascade), the 1-indexed distance from the triggering collaborator, where layer 1 is the immediate neighbor. The head of the chain has no upline. The tail has no downline. Cascades simply stop when they run out of chain. In the admin UI you don't type positions by hand. You drag members into the order you want and Siren recomputes positions on save based on the visual order. Add a new member at the bottom and they take the next position. Remove someone from the middle and the positions below shift up so the chain stays contiguous. Inactive members, suspended or deleted, earn nothing and are skipped, but they do not renumber the layers around them. A member's layer is fixed by their position in the chain, not by how many members happen to be active. If the layer-2 upline is suspended, layer 2 pays no one and the person above them stays at layer 3, credited at the layer-3 score. The cascade keeps walking up the chain past the skipped member and stops only when a layer is set to 0 or the chain runs out. ## When to use it Pick a linear chain when your structure is fundamentally ordered and one-dimensional. The quickest way to check is to ask "who's above this person?" If you can answer with a single name rather than a list, a chain fits. A simple sales-team ladder is the classic case, where rank determines override depth: the newest rep sits at the tail, the regional director at the head, and every sale pays the people above the seller. The same shape covers a single reporting line where a rep rolls up to a manager who rolls up to a director, since that's naturally a chain rather than a tree, as well as a partner chain for tiered programs where each partner sits beneath the one above them, in order. If your structure branches (one person has multiple direct reports, or one manager has many direct reports), you want [parent-child](/documentation/collaborator-group-structures/parent-child) instead. Parent-child encodes a tree, which a linear chain can't represent. If you don't need a hierarchy at all and every member should be treated equally, see the [choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) overview for when flat is the right call. A linear chain pairs naturally with the [upline cascade](/documentation/calculation-strategies/upline-cascade) and [downline cascade](/documentation/calculation-strategies/downline-cascade) calc strategies. Upline cascade walks toward the head when someone makes a sale and pays the people above them. Downline cascade walks toward the tail. ## Configuration In the admin UI: 1. Create a CollaboratorGroup or open an existing one. See [create a collaborator group](/documentation/getting-started/create-a-collaborator-group) for the full walkthrough. 2. Set the structure to Linear Chain. 3. Add members. 4. Drag them into the order you want. Top of the list becomes position 1. 5. Save. Siren writes the positions based on the visual order. In the API, PUT to `/collaborator-groups/{id}/members` with each member carrying `metadata.position` as an integer: ```json { "members": [ { "collaboratorId": 101, "metadata": { "position": 1 } }, { "collaboratorId": 102, "metadata": { "position": 2 } }, { "collaboratorId": 103, "metadata": { "position": 3 } } ] } ``` This PUT replaces the group's full member list. Any member not included in the body is removed from the group, so send every member you want to keep. To add or remove individual members without replacing the list, use the add and remove member endpoints instead. Lower position values sit closer to the head of the chain. The triggering collaborator is never credited by the cascade itself, only the layers above or below them, depending on which calc strategy is bound. A member whose `position` is missing or non-numeric is read as position 0, which sorts to the top of the chain. Members that share a position keep their existing order, so a group switched to linear chain from another structure starts with every member at position 0, in their current order, until you set real positions. Set positions before any cascade fires, because while members are tied at 0 the chain order is just the order they already had, and overrides would pay out along that incidental order rather than the hierarchy you intend. Every member stays part of the group regardless of position, so a member left at 0 is still in the chain. They just sit at the head until you give them an order. ## List Collaborator Group Members Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/list-members Returns a paginated list of members for a single collaborator group, scoped to the parent group and filterable by collaborator. # List Collaborator Group Members `GET /siren/v1/collaborator-groups/{id}/members` Returns the group's members, paginated and filterable. Results are wrapped in a pagination envelope with `items`, `total`, `page`, `perPage`, and `totalPages`. The supplied `{id}` always scopes the result to that group, so this listing never returns rows from another group. Requires authentication and the `Read` capability on the `CollaboratorGroup` resource. The `fields` value selects from the registered member field resolvers. See [Collaborator Groups (REST)](/documentation/resource-reference/collaborator-groups) for the full member field reference and the `metadata` shape for each structure. A name with no registered resolver (for example a typo, or a field a plugin has not added) is dropped from the response rather than returning an error. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | -- | Comma-separated list of fields to include (required) | | `collaboratorId` | string | -- | Filter by collaborator id (exact match or comma-separated IN) | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field | | `order` | string | `ASC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/collaborator-groups/12/members?fields=id,groupId,collaboratorId,metadata ``` **Example Response:** ```json { "items": [ { "id": 87, "groupId": 12, "collaboratorId": 41, "metadata": { "position": 1 } }, { "id": 88, "groupId": 12, "collaboratorId": 42, "metadata": { "position": 2 } } ], "total": 2, "page": 1, "perPage": 10, "totalPages": 1 } ``` Responds 200 with the resolved members in the `items` array. When the group has no matching members, `items` is empty and `total` is `0`. The same empty envelope comes back when the supplied `{id}` matches no group at all, so an unknown group id is not a separate 404. Returns 500 when the datastore fails to fetch member data. The envelope is derived from your `number` and `offset` query parameters. `perPage` echoes `number`, `page` is `floor(offset / number) + 1`, and `totalPages` is `ceil(total / number)`. The `total` is the same value carried in the `x-siren-estimated-count` response header, and `totalPages` is derived from it, so both are estimates rather than exact counts. Do not use either as a loop bound. To page through every member, hold `number` fixed, advance `offset` by `number`, and stop when a page returns fewer than `number` items. **Events:** broadcasts `CollaboratorGroupMemberResolverRegistryInitiated` so listeners can register additional member field resolvers before the response is built. ## List Collaborator Group Structure Resolvers Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/structures Returns the registry of installed collaborator group structure resolvers. # List Collaborator Group Structure Resolvers `GET /siren/v1/collaborator-groups/structures` Returns the registry of installed structure resolvers wrapped in an `options` envelope. The top-level response is an object with a single `options` key whose value is the array of resolver entries. Unlike the paginated list endpoints, this is a static `options` envelope with no `total`, `page`, or `perPage`, because the registry is a fixed in-memory set rather than a queryable collection. Each entry includes `id`, `name`, `description`, and `providedWalkerCapabilities`, the list of [walker capability](/documentation/general/calc-capability-matching) ids the resolver advertises (for example `hasLayer` for `linearChain` and `parentChild`, and the empty list for `flat`). A structure that advertises `hasLayer` has the ordered layers a cascade needs, so the calc picker on the Program and Distributor edit screens uses `providedWalkerCapabilities` to hide calc strategies the bound group's structure cannot support. The admin structure picker also uses this endpoint. For how that matching works, see [Why some calculation methods disappear](/documentation/general/calc-capability-matching). The registry is the set of resolvers actually installed, so treat both the resolver `id` and the capability ids as an open set rather than a fixed list of three. The `id` is the stable contract, so key all logic off `id` and off the capability ids. The `name` and `description` are human-facing display strings, shown here for illustration only, and may be localized or change between releases, so do not match against them. The `linearChain` and `parentChild` resolvers appear only when the Pro tier is active. A Plus-only install returns `flat` as the single entry. The example response below is from a Pro install, so all three resolvers appear. A group's currently chosen structure is the `structure` field on the group itself, covered in [Collaborator Groups (REST)](/documentation/resource-reference/collaborator-groups). This endpoint takes no request body and no query parameters. It requires authentication and read capability on the `CollaboratorGroup` resource. **Example Request:** ``` GET /siren/v1/collaborator-groups/structures ``` **Example Response:** ```json { "options": [ { "id": "flat", "name": "Flat", "description": "Every member is a peer.", "providedWalkerCapabilities": [] }, { "id": "linearChain", "name": "Linear Chain", "description": "Members ordered into a single chain by position.", "providedWalkerCapabilities": ["hasLayer"] }, { "id": "parentChild", "name": "Parent-Child Tree", "description": "Members arranged into a parent-child tree.", "providedWalkerCapabilities": ["hasLayer"] } ] } ``` Responds 200 with the resolver registry. Responds 401 when the request is not authenticated, and 403 when the current context lacks read capability on `CollaboratorGroup`. ## See also - [Choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure): picker guide for the three built-in structures. - [Flat](/documentation/collaborator-group-structures/flat), [linear chain](/documentation/collaborator-group-structures/linear-chain), and [parent-child tree](/documentation/collaborator-group-structures/parent-child): what each structure means and the metadata it reads. - [Why some calculation methods disappear](/documentation/general/calc-capability-matching): how `providedWalkerCapabilities` gates the calc strategy picker. - [Choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy): which strategy fits a given program. ## List Collaborator Groups Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/list Returns a paginated list of collaborator groups with support for filtering by structure and search. # List Collaborator Groups `GET /siren/v1/collaborator-groups` Returns a paginated list of collaborator groups. Results are wrapped in a pagination envelope with `items`, `total`, `page`, `perPage`, and `totalPages`. All requests require authentication and the `Read` capability on the `CollaboratorGroup` resource. The `fields` value selects from the registered group field resolvers. The core set is `id`, `name`, `description`, and `structure`, and a plugin can add more through the `CollaboratorGroupResolverRegistryInitiated` event. See [Collaborator Groups (REST)](/documentation/resource-reference/collaborator-groups) for the full field reference. A name with no registered resolver is dropped from the response rather than returning an error. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | -- | Comma-separated list of fields to include (required) | | `structure` | string | -- | Filter by structure resolver id, exact match. Values: `flat`, `linearChain`, `parentChild` | | `s` | string | -- | Search across name and description | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field | | `order` | string | `ASC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/collaborator-groups?fields=id,name,structure&s=sales ``` **Example Response:** ```json { "items": [ { "id": 12, "name": "Sales Reps", "structure": "linearChain" } ], "total": 1, "page": 1, "perPage": 10, "totalPages": 1 } ``` Responds 200 with the resolved field set under `items`, alongside pagination metadata (`total`, `page`, `perPage`, `totalPages`). When no rows match, `items` is an empty array and `total` is `0`. A datastore failure responds 500 with the message `Something went wrong fetching collaborator group data`. The envelope is derived from your `number` and `offset` query parameters. `perPage` echoes `number`, `page` is `floor(offset / number) + 1`, and `totalPages` is `ceil(total / number)`. The `total` is the same value carried in the `x-siren-estimated-count` response header, and `totalPages` is derived from it, so both are estimates rather than exact counts. Do not use either as a loop bound. To page through every group, hold `number` fixed, advance `offset` by `number`, and stop when a page returns fewer than `number` items. **Events:** broadcasts `CollaboratorGroupResolverRegistryInitiated` so listeners can register additional field resolvers before the response is built. ## List Collaborators Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/list Retrieve a paginated list of collaborators with filtering, search, and field selection. ### List Collaborators `GET /siren/v1/collaborators` Returns a paginated list of collaborators. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | -- | Comma-separated list of fields to include (required) | | `status` | string | -- | Filter by exact status | | `fullName` | string | -- | Filter by exact full name | | `nickname` | string | -- | Filter by exact nickname | | `email` | string | -- | Filter by exact email | | `s` | string | -- | Search across fullName, nickname, email, and alias codes | | `programId` | integer | -- | Filter to collaborators enrolled in this program | | `distributorId` | integer | -- | Filter to collaborators assigned to this distributor | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field | | `order` | string | `ASC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/collaborators?fields=id,fullName,status,referralCode&status=active&number=25 ``` **Example Response:** ```json [ { "id": 7, "fullName": "Jane Smith", "status": "active", "referralCode": "BCDGHJK" } ] ``` ## List Conversions Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/list Retrieve a paginated list of conversions with filtering and field selection. ### List Conversions `GET /siren/v1/conversions` Returns a paginated list of conversions. Results are wrapped by the `ListResponseWrapperInterceptor`. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `status` | string | -- | Filter by status | | `engagementId` | integer | -- | Filter by engagement ID | | `type` | string | -- | Filter by conversion type | | `obligationId` | integer | -- | Filter by obligation ID | | `transactionId` | integer | -- | Filter by transaction ID | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `engagementId`, `type`, `status`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | #### Response Headers The response includes an `x-siren-estimated-count` header with the total matching record count, exposed via `Access-Control-Expose-Headers`. #### Example Request ``` GET /siren/v1/conversions?status=pending&fields=id,status,type,collaboratorId&number=25 ``` #### Example Response ```json [ { "id": 42, "status": "pending", "type": "sale", "collaboratorId": 7 } ] ``` ## List Distributions Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributions/list Returns a paginated list of distributions with support for filtering by status and distributor. # List Distributions `GET /siren/v1/distributions` Returns a paginated list of distributions. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `status` | string | -- | Filter by distribution status | | `distributorId` | integer | -- | Filter by distributor ID | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `status`, `distributorId`, `triggerDate`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/distributions?status=pending&fields=id,status,distributorName,value&number=25 ``` **Example Response:** ```json [ { "id": 18, "status": "pending", "distributorName": "Monthly Payouts", "value": 5000 } ] ``` ## List Distributors Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors/list Returns a paginated list of distributors with support for filtering by status and search. # List Distributors `GET /siren/v1/distributors` Returns a paginated list of distributors. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `status` | string | -- | Filter by status | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `name`, `status`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | The `name` and `description` columns are searchable via the standard search parameter. **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/distributors?status=active&fields=id,name,status,collaboratorCount&number=25 ``` **Example Response:** ```json [ { "id": 1, "name": "Standard Commission", "status": "active", "collaboratorCount": 12 } ] ``` ## List Engagements Source: https://www.sirenaffiliates.com/documentation/resource-reference/engagements/list Retrieve a paginated list of engagements with filtering and field selection. ### List Engagements `GET /siren/v1/engagements` Returns a paginated list of engagements. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `status` | string | -- | Filter by status | | `collaboratorId` | integer | -- | Filter by collaborator ID | | `programId` | integer | -- | Filter by program ID | | `opportunityId` | integer | -- | Filter by opportunity ID | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/engagements?status=active&fields=id,status,score,collaboratorName&number=25 ``` **Example Response:** ```json [ { "id": 91, "status": "active", "score": 100, "collaboratorName": "Jane Smith" } ] ``` ## List Fulfillments Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments/list Returns a paginated list of fulfillments with filtering, sorting, and field selection. ### List Fulfillments `GET /siren/v1/fulfillments` Returns a paginated list of fulfillments. Results are wrapped by the `ListResponseWrapperInterceptor`. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `status` | string | -- | Filter by fulfillment status | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `status`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | The response includes an `x-siren-estimated-count` header with the total number of matching records. #### Example ``` GET /siren/v1/fulfillments?status=pending&fields=id,status,payoutCount,totalValue ``` ```json [ { "id": 5, "status": "pending", "payoutCount": 12, "totalValue": 45000 } ] ``` ## List Notes for a Source Source: https://www.sirenaffiliates.com/documentation/resource-reference/notes/list Retrieve the activity feed for a given source record. ### List Notes for a Source `GET /siren/v1/notes` Returns the activity feed for a single source record — every note linked to the given `sourceType` / `sourceId` pair, in chronological order. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `sourceType` | string | _required_ | The entity type to fetch notes for (for example, `collaborator`, `obligation`, `conversion`, `fulfillment`, `payout`, `engagement`, `transaction`). | | `sourceId` | integer | _optional_ | The entity ID. Omit to retrieve every note of the given `sourceType` across the install. | | `fields` | string | `id,renderedContent,creationSource,dateCreated` | Comma-separated list of fields to include. Extended fields (`renderedContent`, `sourceData`, `sources`) only resolve when requested here. | | `search` | string | -- | Free-text search. Matches against the resolved blueprint content. | | `from` | string | -- | ISO-8601 datetime. Lower bound on `dateCreated`. | | `to` | string | -- | ISO-8601 datetime. Upper bound on `dateCreated`. | | `number` | integer | 20 | Results per page | | `offset` | integer | 0 | Pagination offset | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC`, always on `dateCreated`. | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/notes?sourceType=conversion&sourceId=118&fields=id,renderedContent,creationSource,dateCreated&number=25 ``` **Example Response:** ```json [ { "id": 904, "renderedContent": "Conversion #118 approved", "creationSource": "conversion", "dateCreated": "2026-04-14T17:02:11+00:00" }, { "id": 903, "renderedContent": "Conversion #118 awarded — saleTransactionPercentage", "creationSource": "transaction", "dateCreated": "2026-04-14T16:58:42+00:00" } ] ``` **Custom formatting:** Request `sourceData` and `sources` instead of `renderedContent` when you want to build your own display. `content` holds the raw blueprint key (for example, `conversion_approved`), `sourceData` is the data array the blueprint was resolved against, and `sources` is the list of `{sourceType, sourceId}` links for the note. ``` GET /siren/v1/notes?sourceType=conversion&sourceId=118&fields=id,content,sourceData,sources,dateCreated ``` ## List Obligations Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations/list Returns a paginated list of obligations with filtering, sorting, and field selection. ### List Obligations `GET /siren/v1/obligations` Returns a paginated list of obligations. Results are wrapped by the `ListResponseWrapperInterceptor`. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `collaboratorId` | integer | -- | Filter by collaborator ID | | `status` | string | -- | Filter by obligation status | | `awardType` | string | -- | Filter by award type | | `payoutId` | integer | -- | Filter by payout ID | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `collaboratorId`, `status`, `awardType`, `value`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | #### Middleware `AutoFilterByOwnerMiddleware` automatically filters by `collaboratorId` for collaborator-role users, and `CollaboratorAliasResolverMiddleware` resolves collaborator aliases before filtering. #### Response Headers The `x-siren-estimated-count` header returns the total number of matching records. #### Example Request ``` GET /siren/v1/obligations?status=pending&fields=id,status,value,collaboratorName&number=25 ``` #### Example Response ```json [ { "id": 10, "status": "pending", "value": 1500, "collaboratorName": "Jane Doe" } ] ``` ## List Payouts Source: https://www.sirenaffiliates.com/documentation/resource-reference/payouts/list Returns a paginated list of payouts with filtering, sorting, and field selection. ### List Payouts `GET /siren/v1/payouts` Returns a paginated list of payouts. Results are wrapped by the `ListResponseWrapperInterceptor`. #### Query Parameters | Parameter | Type | Default | Description | |---|---|---|---| | `fulfillmentId` | integer | -- | Filter by parent fulfillment | | `collaboratorId` | integer | -- | Filter by collaborator | | `status` | string | -- | Filter by payout status (`paid`, `unpaid`) | | `currency` | string | -- | Filter by currency code | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `fulfillmentId`, `collaboratorId`, `status`, `value`, `currency`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | This endpoint runs `CollaboratorAliasResolverMiddleware` to resolve collaborator aliases before filtering. The response includes an `x-siren-estimated-count` header with the total number of matching records. #### Example ``` GET /siren/v1/payouts?fulfillmentId=5&status=unpaid&fields=id,value,collaboratorName ``` ```json [ { "id": 20, "value": 3000, "collaboratorName": "Jane Doe" } ] ``` ## List Program Groups Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups/list Returns a paginated list of program groups with support for filtering by sorting strategy and search. # List Program Groups `GET /siren/v1/program-groups` Returns a paginated list of program groups. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | -- | Comma-separated list of fields to include (required) | | `sorter` | string | -- | Filter by sorting strategy | | `s` | string | -- | Search across name and description | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field | | `order` | string | `ASC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/program-groups?fields=id,name,sorter&s=affiliate ``` **Example Response:** ```json [ { "id": 1, "name": "Affiliate Tiers", "sorter": "oldestBindingWins" } ] ``` ## List Programs Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/list Returns a paginated list of programs, with support for filtering by status, incentive type, currency, and program group. # List Programs `GET /siren/v1/programs` Returns a paginated list of programs. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `fields` | string | -- | Comma-separated list of fields to include (required) | | `status` | string | -- | Filter by status | | `incentiveType` | string | -- | Filter by incentive type | | `incentiveResolverType` | string | -- | Filter by incentive resolver type | | `units` | string | -- | Filter by currency unit | | `programGroupId` | integer | -- | Filter to programs belonging to a specific program group | | `s` | string | -- | Search across `name` and `description` | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field | | `order` | string | `ASC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/programs?fields=id,name,status,collaboratorCount&status=active&number=25 ``` **Example Response:** ```json [ { "id": 1, "name": "Standard Affiliate Program", "status": "active", "collaboratorCount": 12 } ] ``` ## List Transactions Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions/list Returns a paginated list of transactions with filtering, sorting, and field selection. ### List Transactions `GET /siren/v1/transactions` Returns a paginated list of transactions. Results are wrapped by the `ListResponseWrapperInterceptor`. **Query Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `status` | string | -- | Filter by status | | `fields` | string | core fields | Comma-separated list of fields to include | | `include` | string | -- | Legacy: `extended` to include extra fields | | `number` | integer | 10 | Results per page | | `offset` | integer | 0 | Pagination offset | | `orderBy` | string | `id` | Sort field. Allowed: `id`, `status`, `dateCreated` | | `order` | string | `DESC` | Sort direction: `ASC` or `DESC` | **Response Headers:** - `x-siren-estimated-count`. Total matching records (exposed via `Access-Control-Expose-Headers`). **Example Request:** ``` GET /siren/v1/transactions?status=complete&fields=id,status,totalValue,currency&number=25 ``` **Example Response:** ```json [ { "id": 101, "status": "complete", "totalValue": 9999, "currency": "USD" } ] ``` ## Listeners & Event Handlers Source: https://www.sirenaffiliates.com/documentation/extensions/listeners Implementing CanHandle to react to Siren domain events with dependency injection. # Listeners & Event Handlers Siren is event-driven. Business logic flows through events and handlers, not step-by-step service calls. Listeners are the primary way extensions react to things happening in the system. Examples include a sale completing, the application booting, and a collaborator registering. This guide covers how to write listeners, register them in your extension, and use dependency injection to access Siren's services from within your handler logic. ## What interface do listeners implement? Every listener implements `PHPNomad\Events\Interfaces\CanHandle` (see [PHPNomad's event listener docs](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners)). The interface is generic-templated, meaning you declare which event type you handle: ```php */ class DoSomethingOnReady implements CanHandle { public function handle(Event $event): void { // Your logic here. Runs once when the app is ready. } } ``` The `@implements` annotation is a docblock convention for IDE support. At runtime, the `handle()` method receives whatever event was dispatched. ## How do I use dependency injection in listeners? Listeners are resolved through the DI container, so you can type-hint any registered service in your constructor and it will be injected automatically: ```php collaborators = $collaborators; $this->config = $config; $this->logger = $logger; } public function handle(Event $event): void { // $this->collaborators, $this->config, $this->logger // are all available here, fully wired by the container. } } ``` This is a major advantage of listeners over raw callbacks. You get clean access to datastores, services, and facades without manually resolving anything. A real example from Siren's codebase is `DisplayAdminNotices`, which injects four services: ```php class DisplayAdminNotices implements CanHandle { protected AdminNoticeProvider $notices; protected CanRender $template; protected LoggerStrategy $logger; protected CanResolvePaths $pathResolver; public function __construct( AdminNoticeProvider $provider, CanRender $template, LoggerStrategy $loggerStrategy, CanResolvePaths $pathResolver ) { $this->pathResolver = $pathResolver; $this->logger = $loggerStrategy; $this->template = $template; $this->notices = $provider; } public function handle(Event $event): void { // Uses all four injected services to render admin notices } } ``` ## How do I register listeners? Listeners are registered via the `getListeners()` method on any class that implements `HasListeners`. In your extension's Integration class or an Initializer, return a mapping of event class to handler class(es): ```php use PHPNomad\Core\Events\Ready; use PHPNomad\Events\Interfaces\HasListeners; class Integration implements Extension, HasListeners { public function getListeners(): array { return [ Ready::class => [ MyFirstListener::class, MySecondListener::class, ], ]; } } ``` The return type is `array, class-string[]|class-string>`. You can map a single handler or an array of handlers to each event. ### Single handler shorthand When you only have one handler for an event, you can skip the array wrapper: ```php public function getListeners(): array { return [ FulfillmentGenerationInitiated::class => GenerateFulfillments::class, PayoutExportInitiated::class => GeneratePayoutFile::class, ]; } ``` ### Multiple handlers for the same event Multiple handlers for the same event is common. Here is a real example from `Loader`: ```php public function getListeners(): array { return [ Ready::class => [ RegisterBlocks::class, SignupFormInitializer::class, SetupCollaboratorAdmin::class, EnqueueReactAdminAssets::class, ], UserPermissionsInitialized::class => [ RegisterRoles::class, SetUserPermissions::class, ], ]; } ``` All four `Ready` listeners will fire when the `Ready` event is broadcast. ## How Listeners Are Loaded Under the hood, the framework's `CanLoadInitializers` trait processes `getListeners()` like this: ```php if ($initializer instanceof HasListeners) { $events = $this->container->get(EventStrategy::class); foreach ($initializer->getListeners() as $event => $handlers) { foreach (Arr::wrap($handlers) as $handler) { $events->attach( $event, fn(Event $event) => $this->container->get($handler)->handle($event) ); } } } ``` The handler is not instantiated until the event fires — `$this->container->get($handler)` runs inside the callback, so constructor DI happens at dispatch time rather than registration time. You never instantiate listeners yourself; the container handles constructor injection automatically. Events are keyed by their fully-qualified class name (`Event::getId()` returns a static string identifier). ## What are common listener patterns? ### Pattern 1: The AdminHandler (WordPress Hook Registration) The most common pattern in WordPress extensions: listen to the `Ready` event, then register WordPress hooks inside `handle()`. This gives you access to injected services while deferring WordPress hook registration until the application is fully initialized. ```php class SetupCollaboratorAdmin implements CanHandle { protected CollaboratorDatastore $collaborators; protected ConfigDatastore $config; protected RenderService $renderService; public function __construct( CollaboratorDatastore $collaborators, ConfigDatastore $config, RenderService $renderService ) { $this->collaborators = $collaborators; $this->config = $config; $this->renderService = $renderService; } public function handle(Event $event): void { // Register WordPress hooks with full access to injected services add_action('wp_dashboard_setup', fn() => $this->addWidgets()); add_action('admin_menu', fn() => $this->loadSubmenus()); add_action('admin_enqueue_scripts', fn($hook) => $this->enqueueScripts($hook)); add_filter('show_admin_bar', fn($current) => $this->maybeShowAdminBar($current)); } } ``` This is why listeners matter for WordPress extensions. Without them, you would need to resolve services from the container manually inside each hook callback. With listeners, constructor DI gives you everything you need. ### Pattern 2: Domain Event Reaction React to something that happened in Siren's domain layer: ```php class UpdateEngagementStatusWhenConverted implements CanHandle { protected EngagementDatastore $engagements; public function __construct(EngagementDatastore $engagements) { $this->engagements = $engagements; } public function handle(Event $event): void { if ($event instanceof ConversionsAwarded) { // Update engagement records when conversions are awarded } } } ``` ### Pattern 3: Feature Registration Register capabilities when a registry event fires: ```php class RegisterCoreEngagementTriggerStrategies implements CanHandle { protected ExtensionRegistryService $extensionRegistryService; public function __construct(ExtensionRegistryService $extensionRegistryService) { $this->extensionRegistryService = $extensionRegistryService; } public function handle(Event $event): void { if ($event instanceof EngagementTriggerRegistryInitiated) { $event->addStrategy(ReferredSiteVisit::class); $event->addStrategy(Manual::class); // Conditionally add based on active extension features if ($this->extensionRegistryService->extensionsSupportFeatures(Features::Coupons)) { $event->addStrategy(BoundCouponUsed::class); } } } } ``` This pattern is used extensively for engagement triggers, conversion types, and metric strategies. The registry event carries an `addStrategy()` method that handlers call to register their contributions. ### Pattern 4: Third-Party Plugin Initialization Initialize add-ons for external plugins at the right time: ```php class InitializeGravityFormsAddon implements CanHandle { protected SirenGFAddOn $addon; public function __construct(SirenGFAddOn $addon) { $this->addon = $addon; } public function handle(Event $event): void { add_action('gform_loaded', function() { \GFAddOn::register(SirenGFAddOn::class); }, 5); } } ``` ## Can multiple listeners handle the same event? Multiple initializers can each register listeners for the same event. The framework iterates over all initializers and attaches all handlers. For example, `Ready` typically has listeners from: - `Siren\WordPress\Core\Strategies\Initializer` (capabilities) - `Siren\WordPress\Integration\Loader` (blocks, admin screens, signup forms) - `Siren\Engagements\Service\Initializer` (trigger strategy registration) - Your extension (whatever you need) All of them fire when `Ready` is broadcast. There is no conflict. ## In what order do listeners execute? Listeners execute in **registration order**. They fire in the order their initializers are loaded, and within an initializer, in the order they appear in the `getListeners()` array. The `EventStrategy::attach()` method accepts an optional `?int $priority` parameter, but the framework's `CanLoadInitializers` does not pass a priority when attaching listeners from `getListeners()`. This means all listeners registered this way run at default priority. If you need to guarantee ordering between your own listeners, list them in the desired order in your `getListeners()` array. If you need ordering relative to other initializers, consider the initializer loading sequence (core loads before extensions). ## How do listeners differ from event bindings? Do not confuse `getListeners()` with `getEventBindings()`. They serve different purposes: `getListeners()` maps domain events to handler classes — it's for reacting to things that happen inside the application (conversions awarded, app ready, records created). `getEventBindings()` maps domain events to platform hooks — it connects WordPress actions (like `woocommerce_new_order`) to Siren events (like `SaleTriggered`), which is how the platform layer translates external triggers into domain events. Your extension will typically use `getEventBindings()` to bridge external hooks into Siren events, and `getListeners()` to react to those events with business logic. ## Managing Collaborators (Affiliates) Source: https://www.sirenaffiliates.com/documentation/getting-started/managing-collaborators-affiliates Step-by-step instructions on how to set up a collaborator account manually. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; import StepList from "@/components/content/StepList.astro"; ## What is a collaborator? A [collaborator](/documentation/general/what-is-a-collaborator) is anyone who participates in one of your incentive [programs](/documentation/general/what-are-programs). Siren uses "collaborator" instead of "affiliate" because affiliates are just one type. A collaborator might be an affiliate sharing referral links, a course creator earning royalties, a blogger paid when their content contributes to a sale, or a salesperson closing deals through your site. The label changes, but the mechanics are the same: they perform tracked actions, and they earn rewards when those actions lead to a [conversion](/documentation/general/what-is-a-conversion). ## Creating a collaborator manually Go to Siren > Collaborators in the WordPress sidebar and click Add New. Fill in the collaborator's name, nickname, and email. Each collaborator gets a tracking ID, which is the unique code used in their affiliate link. Siren generates one automatically, but you can change it to anything memorable. Old tracking IDs keep working even if you change them later, so existing links won't break. Set the status to Active so they can start creating [engagements](/documentation/general/what-is-an-engagement) immediately. Pending or Inactive collaborators can't generate engagements or earn anything. Finally, pick which programs to enroll them in. They only earn from programs they're in, so double-check before clicking Create. A collaborator can also be reached through a [collaborator group](/documentation/general/what-are-collaborator-groups) instead of, or in addition to, direct program enrollment. When a program or distributor is bound to a group, every member of that group becomes eligible for it without per-collaborator enrollment. See [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group) to set one up. Collaborators", description: "Open the Collaborators screen." }, { title: "Click Add New", description: "Opens a blank collaborator form." }, { title: "Enter name, nickname, and email" }, { title: "Review the tracking ID", description: "Auto-generated, but you can change it anytime. Old IDs keep working." }, { title: "Set status to Active", description: "Pending or Inactive collaborators can't earn." }, { title: "Select programs to enroll them in" }, { title: "Click Create" }, ]} /> ## Manual creation vs. self-registration Manual creation is for inviting specific people you already know. Self-registration uses a [registration form](/documentation/getting-started/set-up-a-program-registration-form) on your site where anyone can sign up, which is how you run an open program. Both paths create the same collaborator record. The difference is who starts the process and whether approval is automatic or manual. ## Sending login credentials Siren creates collaborator records separately from WordPress user accounts. A collaborator can exist and earn commissions without ever logging in. If you want them to access the [collaborator dashboard](/documentation/getting-started/the-collaborator-dashboard) for stats and referral links, they need a WordPress user account. Before you can create user accounts, WordPress has to allow registrations. Go to Settings > General and check "Anyone can register." Without this, Siren can't create accounts for your collaborators. Then go to Siren > Collaborators, check the collaborator, choose "Send login email" from the bulk actions dropdown, and click Apply. They'll get an email with a link to finish setup and create a username and password. General", description: "Check \"Anyone can register\" and save." }, { title: "Go to Siren > Collaborators" }, { title: "Check the box next to the collaborator" }, { title: "Select \"Send login email\" and click Apply", description: "They receive an email with a setup link." }, { title: "The collaborator creates a username and password", description: "After that, they can log in to the dashboard." }, ]} /> ## Managing status and program enrollment Click any collaborator on the Siren > Collaborators screen to edit their profile. You can change status between Active, Pending, and Inactive, and adjust program enrollments. Setting a collaborator to Inactive stops new engagements immediately but leaves existing obligations and pending payouts alone. Commissions they've already earned stay put regardless of current status. Deleting a collaborator also removes them from every [collaborator group](/documentation/general/what-are-collaborator-groups) they belonged to. If that collaborator sat in the middle of a chain or tree feeding a cascade, removing them re-shapes the chain for everyone below before the next sale fires. For how membership changes affect a live cascade, see [Operating a cascade program](/documentation/getting-started/operating-a-cascade-program). You can also view engagement history and obligations from the profile for a complete picture of their activity. The profile's [activity feed](/documentation/general/activity-feeds) is the fastest place to read that picture chronologically — every conversion, obligation, and payout tied to the collaborator is listed there in order, which is especially useful when someone emails asking about a specific transaction. The video walks through creating a collaborator manually from Siren > Collaborators. It covers the basic fields (name, nickname, email), the auto-generated tracking ID and how old IDs continue to work after changes, and selecting which programs the collaborator belongs to. It then demonstrates sending a login email so the collaborator can access the dashboard. This requires enabling "Anyone can register" under Settings > General first. Once that's set, the bulk action "Send login email" delivers a setup link. The collaborator clicks the link, picks a username and password, and can log in to view their stats. ## Manual Attribution Source: https://www.sirenaffiliates.com/documentation/general/manual-attribution When an admin manually attributes a transaction to a collaborator, the Manual Attribution event is triggered. When automatic tracking misses a referral, an admin can manually attribute a [transaction](/documentation/general/what-are-transactions) to a [collaborator](/documentation/general/what-is-a-collaborator). This triggers the Manual Attribution event, which creates [engagements](/documentation/general/what-is-an-engagement) for each of the collaborator's active [programs](/documentation/general/what-are-programs) that have the manual trigger enabled. Unlike other engagement events that fire automatically when a customer does something (clicks a link, uses a coupon, submits a form), manual attribution is initiated by a person through the Siren admin or the REST API. The pipeline that follows is the same. Engagements are created, programs evaluate whether a conversion should be awarded, and [obligations](/documentation/general/what-are-obligations) are calculated based on the program's incentive structure. Admin Credits a Transaction An admin selects one or more transactions and attributes them to a collaborator, either through the Transactions bulk action in WordPress or the REST API. Opportunity Created Siren creates an [opportunity](/documentation/general/what-is-an-opportunity) to represent this attribution event, connecting the collaborator to the transaction. Engagements Created For each active program the collaborator is enrolled in that has the manual trigger enabled, an engagement is created. Engagement Points Added Points get added to each engagement, depending on the value set for the manual trigger in each [program](/documentation/general/what-are-programs). Conversions Created The system evaluates the engagements and creates [conversions](/documentation/general/what-is-a-conversion) for qualifying programs, just as it would for any other attribution event. ## When to enable the manual trigger Enable the manual attribution trigger on programs where you might need to handle sales that happen outside of normal tracking channels. Common situations include phone or email orders where the customer mentions a collaborator, orders where the customer's tracking cookie expired before purchasing, sales that happened through a channel Siren doesn't track (like an in-person event), and corrections when attribution was missed or assigned incorrectly. Programs that only use automated tracking (referral links, coupon codes) don't need this trigger enabled. Keeping it disabled prevents accidental manual attribution on programs where it wouldn't make sense. ## How to attribute a transaction For step-by-step instructions on attributing transactions from the WordPress admin or via the API, see the [manually attribute a transaction guide](/documentation/getting-started/manually-attribute-a-transaction). ## ManualAttributionRequested Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/manual-attribution-requested Fires when an admin manually attributes a conversion to a specific collaborator, bypassing engagement-based attribution. # ManualAttributionRequested Sometimes attribution cannot be determined through tracked engagements. A phone order, a deal closed over email, or a situation where tracking failed all require manual intervention. When an admin manually attributes a conversion to a specific collaborator, the system fires `ManualAttributionRequested`, bypassing the normal engagement-based attribution flow entirely. The event is identified as `manual_attribution_requested` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The event carries three models that together provide everything the manual attribution listener needs to create conversions without relying on tracked engagements: a `Collaborator` (who should receive credit), a `Transaction` (the financial details), and an `Opportunity` (the attribution context). ```php use Siren\Conversions\Core\Events\ManualAttributionRequested; public function handle(Event $event): void { $collaborator = $event->collaborator; $transaction = $event->transaction; $opportunity = $event->opportunity; } ``` Note that unlike most Siren events, the data on this event is accessed through public properties rather than getter methods. ## How does this differ from the normal flow? In the standard conversion pipeline, `ConversionInitialized` starts from an opportunity and works through engagements to determine which collaborators should receive credit. Manual attribution skips that entire chain. The admin has already decided who gets credit, so the system only needs the collaborator identity, the transaction details, and an opportunity to anchor the conversion to. This makes `ManualAttributionRequested` a parallel entry point into the conversion system. It rejoins the standard pipeline once conversions are created, with the same approval, obligation, and fulfillment steps applying from that point forward. For a user-level walkthrough of when and how to use manual attribution, see [Manually Attribute a Transaction](/documentation/getting-started/manually-attribute-a-transaction). See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## Manually Attribute a Transaction Source: https://www.sirenaffiliates.com/documentation/getting-started/manually-attribute-a-transaction Step-by-step instructions for manually attributing a transaction to a collaborator when automatic tracking missed the referral. import StepList from "@/components/content/StepList.astro"; Manual attribution is one of Siren's [engagement trigger types](/documentation/general/manual-attribution), just like site visits, coupon codes, and form submissions. The difference is that a person initiates it rather than a customer action. Sometimes a collaborator drives a sale that Siren's automatic tracking doesn't capture. Maybe the customer called in to place an order, or the referral happened through a channel that isn't tracked, or the customer cleared their cookies before purchasing. When this happens, you can manually attribute the transaction to the collaborator who deserves credit. ## How manual attribution works Manual attribution takes an existing [transaction](/documentation/general/what-are-transactions) and runs it through the full attribution pipeline as if the collaborator had referred the customer. If the transaction doesn't exist in Siren yet (because it happened outside of your commerce plugin), you'll need to [create it manually](/documentation/getting-started/creating-transactions-manually) first. Siren creates an [opportunity](/documentation/general/what-is-an-opportunity), triggers [engagements](/documentation/general/what-is-an-engagement) for each of the collaborator's active programs, and then creates [conversions](/documentation/general/what-is-a-conversion) and [obligations](/documentation/general/what-are-obligations) based on the program rules. This is not the same as manually creating a conversion record. Manual attribution uses the real pipeline, which means program rules, [program groups](/documentation/general/what-are-program-groups), incentive calculations, and [line item filters](/documentation/general/line-item-filters) all apply normally. The only difference is that a person initiated the attribution instead of a referral link or coupon code. ## Attributing from the WordPress admin The Transactions screen in the Siren admin has a built-in bulk action for manual attribution. Transactions", description: "Open the Transactions screen in your WordPress admin." }, { title: "Select your transactions", description: "Check the boxes next to one or more transactions you want to attribute." }, { title: "Choose \"Credit Collaborator\"", description: "Select it from the bulk actions dropdown and click Apply." }, { title: "Enter the collaborator and attribution type", description: "Provide the collaborator ID and select the type (usually \"sale\")." }, { title: "Submit", description: "Siren runs each transaction through the full attribution pipeline, creating engagements, conversions, and obligations based on program rules." }, ]} /> You can attribute multiple transactions at once. This is useful when catching up on a batch of orders that weren't tracked, like phone orders or orders placed through a channel that doesn't have integration support. ## Attributing through the API Manual attribution is also available through the REST API using the Credit Collaborator endpoint on the [Transactions API](/documentation/resource-reference/transactions). This triggers the same full pipeline as the admin bulk action and is useful for integrating manual attribution into custom workflows or external tools. ## When to use manual attribution Manual attribution is the right tool when a real transaction exists but automatic tracking missed the referral. Common situations include orders placed by phone or email where the customer mentioned a collaborator, orders where the customer's tracking cookie expired before they purchased, sales that happened through a channel Siren doesn't track (like an in-person event), and corrections when a referral was attributed to the wrong collaborator and needs to be reassigned. If you need to pay a collaborator for something that isn't tied to a transaction at all, like a signing bonus or a negotiated flat payment, creating an obligation directly may be more appropriate than manual attribution. ## What happens after attribution Once you attribute a transaction, the conversion and obligation follow the normal lifecycle. The conversion starts in a pending state and needs to be approved (unless your program has auto-approval enabled). After approval, the obligation is created with the calculated reward amount based on the program's incentive structure. The obligation then waits for inclusion in a [fulfillment](/documentation/general/what-is-a-fulfillment) like any other payout. If the collaborator is enrolled in multiple programs, manual attribution can create multiple conversions and obligations from the same transaction, just like automatic attribution would. To confirm the attribution went where you expected, open the transaction or the collaborator and read the [activity feed](/documentation/general/activity-feeds). Manual attributions write an entry each time they run, so you can verify the conversions and obligations that were generated without clicking through to each one individually. ## Mappings Source: https://www.sirenaffiliates.com/documentation/resource-reference/mappings Accessing and querying Siren mappings — the bridge between Siren entity IDs and external platform IDs — through the PHP data layer. import CodeTabs from "@/components/content/CodeTabs.astro"; # Mappings A mapping in Siren links a local Siren entity to an entity in an external system. Every time Siren needs to know "which WooCommerce order does this conversion correspond to?" or "which Stripe customer is this collaborator?", it looks up a mapping. The mapping table stores these relationships as pairs of IDs qualified by type strings, so the same table can bridge any combination of local and external resources. Mappings are one of the most integration-critical datastores in Siren. If you're building a custom integration with a payment gateway, CRM, or e-commerce platform, you'll use mappings to track which records on each side correspond to each other. Unlike most other Siren datastores, mappings use a compound key (the combination of localId, externalId, localType, and externalType) rather than a single auto-incrementing primary key. > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Siren's built-in integrations (WooCommerce, EDD, LifterLMS, etc.) create mappings automatically as part of their event handlers. If you're building a custom integration, you'll create mappings in your own event handlers. This typically happens when processing an inbound webhook or synchronizing records. ## Accessing mapping data The mapping datastore is available through dependency injection or the static facade. ```php use Siren\Mappings\Core\Datastores\Mapping\Interfaces\MappingDatastore; class OrderSync { protected MappingDatastore $mappings; public function __construct(MappingDatastore $mappings) { $this->mappings = $mappings; } public function getSirenConversionForOrder(int $wcOrderId): ?\Siren\Mappings\Core\Models\Mapping { try { return $this->mappings->getByExternalId($wcOrderId, 'wc_order', 'conversion'); } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { return null; } } } ``` ```php use Siren\Mappings\Core\Facades\Mappings; try { $mapping = Mappings::getByExternalId($wcOrderId, 'wc_order', 'conversion'); $conversionId = $mapping->getLocalId(); } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { // This order hasn't been processed by Siren yet } ``` ## The mapping model Each mapping record is represented by a `Mapping` model instance. | Field | Type | Description | |---|---|---| | `localId` | int | The ID of the Siren entity | | `externalId` | int or string | The ID from the external system | | `localType` | string | What kind of Siren entity this is (e.g., `conversion`, `collaborator`, `transaction`) | | `externalType` | string | What kind of external entity this is (e.g., `wc_order`, `stripe_customer`, `edd_payment`) | Getter methods: `getLocalId()`, `getExternalId()`, `getLocalType()`, `getExternalType()`. The identity of a mapping is the full compound key. All four fields together form the identity. There is no single `id` column. The `getIdentity()` method returns an array with all four fields. ## Available methods The mapping datastore provides the standard CRUD methods documented in the [introduction](/documentation/resource-reference/introduction). It adds several domain-specific methods designed for the common lookup patterns integrations need. ### Finding a specific mapping `find` retrieves a single mapping by all four compound key fields. This is the most precise lookup. Use it when you know both the local and external identifiers and need to confirm the mapping exists. This method is available on the datastore interface through dependency injection. ```php use Siren\Mappings\Core\Datastores\Mapping\Interfaces\MappingDatastore; // Inside a service class with MappingDatastore injected try { $mapping = $this->mappings->find(42, 'conversion', 1001, 'wc_order'); // The mapping exists -- this WC order is already linked to this conversion } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { // No mapping exists for this combination } ``` ### Looking up by local ID `getByLocalId` finds the mapping for a Siren entity when you need to discover which external record it corresponds to. You provide the local ID, local type, and the external type you're looking for. ```php use Siren\Mappings\Core\Facades\Mappings; // Find the WooCommerce product linked to a Siren collaborator coupon try { $mapping = Mappings::getByLocalId($collaboratorId, 'collaborator', 'wc_coupon'); $wcCouponId = $mapping->getExternalId(); } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { // This collaborator doesn't have a WC coupon mapped } ``` ### Looking up by external ID `getByExternalId` is the reverse lookup. You have an external system's ID and need to find the corresponding Siren entity. This is the most common lookup in inbound webhook handlers. ```php use Siren\Mappings\Core\Facades\Mappings; // When processing an incoming WooCommerce order webhook, // check if this order has already been processed try { $mapping = Mappings::getByExternalId($wcOrderId, 'wc_order', 'conversion'); // Already processed -- the conversion ID is $mapping->getLocalId() } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { // New order -- process it and create the mapping } ``` ### Deleting mappings for a local entity `deleteMappingsForLocalId` removes all mappings for a given local entity, regardless of which external systems they point to. This is useful during cleanup or when a Siren entity is being removed. ```php use Siren\Mappings\Core\Facades\Mappings; // Remove all external mappings for a collaborator Mappings::deleteMappingsForLocalId($collaboratorId, 'collaborator'); ``` ### Deleting mappings for an external entity `deleteMappingsForExternalId` removes all mappings pointing to a specific external entity. Use this when the external record is being deleted or when you need to unlink an external resource from Siren. ```php use Siren\Mappings\Core\Facades\Mappings; // Unlink a WooCommerce coupon from all Siren entities Mappings::deleteMappingsForExternalId($wcCouponId, 'wc_coupon'); ``` ### Deleting a specific mapping `delete` removes a single mapping identified by all four compound key fields. Unlike `deleteMappingsForLocalId` and `deleteMappingsForExternalId`, this targets exactly one relationship. This method is available on the datastore interface through dependency injection. ```php use Siren\Mappings\Core\Datastores\Mapping\Interfaces\MappingDatastore; // Inside a service class with MappingDatastore injected $this->mappings->delete(42, 'conversion', 1001, 'wc_order'); ``` ## Practical patterns ### Checking if an order has been processed The most common integration pattern is checking whether an inbound event has already been handled. Before creating a new conversion from a WooCommerce order, check whether a mapping already exists. ```php use Siren\Mappings\Core\Facades\Mappings; function hasOrderBeenProcessed(int $wcOrderId): bool { try { Mappings::getByExternalId($wcOrderId, 'wc_order', 'conversion'); return true; } catch (\PHPNomad\Datastore\Exceptions\RecordNotFoundException $e) { return false; } } ``` ### Linking a WooCommerce product to a collaborator When building custom integration logic that needs to associate external platform records with Siren entities, create a mapping to track the relationship. ```php use Siren\Mappings\Core\Facades\Mappings; // After creating a WooCommerce coupon for a collaborator, // store the mapping so Siren can resolve it later Mappings::create([ 'localId' => $collaboratorId, 'localType' => 'collaborator', 'externalId' => $wcCouponId, 'externalType' => 'wc_coupon' ]); ``` ## MetricsTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-distributions/metrics-triggered Fires when new metric data is recorded from sale, visit, or other trigger strategies. # MetricsTriggered `MetricsTriggered` fires when new metric data is recorded from a sale, a visit, or another trigger strategy. Metrics are the raw performance numbers that distributions use to calculate each collaborator's share of a reward pool. Every time a tracked interaction produces measurable data, this event captures it. The event ID is `metrics_triggered`, and its fully qualified class is `Siren\Metrics\Core\Events\MetricsTriggered`. ## What does this event carry? The event carries two things: a `metrics` array containing the recorded data points, and a `strategyId` string that identifies which trigger strategy produced the data. The strategy ID allows downstream listeners to filter for specific metric types when building distribution calculations. ```php use Siren\Metrics\Core\Events\MetricsTriggered; public function handle(Event $event): void { $metrics = $event->getMetrics(); $strategyId = $event->getStrategyId(); // Each metric entry in the array contains the measured values // that will be accumulated until the next distribution period } ``` ## How do metrics feed into distributions? Metrics accumulate continuously between distribution periods. When a `DistributionCompleted` event fires, the distribution system reads the accumulated metrics for each collaborator enrolled in the relevant programs and uses them to calculate proportional shares of the reward pool. After the distribution is processed, metrics are reset for the next period. ## What about manual metrics? Sometimes metric data needs to be recorded outside of an automated trigger. The `ManualMetricRequested` event (event ID: `manual_metric_requested`) handles this case. It fires when an administrator or an API call explicitly attributes metric data to a collaborator. The downstream effect is the same: the metric values accumulate and contribute to the next distribution calculation. The distinction exists so that listeners can differentiate between automatically tracked and manually attributed metrics when needed. ## What about cascade metrics? Distributors configured with a cascade metric calculation strategy can emit more than one metric per trigger. The trigger service resolves the distributor's `MetricCalculationStrategy` for the fired metric type, runs it, and persists one metric row per result against the distributor's current distribution. With the first-party [Upline Cascade](/documentation/calculation-strategies/upline-cascade) or [Downline Cascade](/documentation/calculation-strategies/downline-cascade) calc strategies bound to a hierarchical [collaborator group](/documentation/general/what-are-collaborator-groups), a single triggering event produces a metric row per credited layer. Listeners that handle `MetricsTriggered` see the full result set in one event payload. See [walker capabilities](/documentation/extensions/walker-capabilities) for the contract custom cascade calcs implement. For the full lifecycle of distribution events and how they connect to the fulfillment pipeline, see the [Distribution Events overview](/documentation/developer-reference/events-distributions). ## Migrate a Tiered Program Group to a Cascade Source: https://www.sirenaffiliates.com/documentation/migration/migrate-tiered-program-group-to-cascade Convert a two-program tiered program group into a single program bound to a linear-chain collaborator group with an Upline Cascade. # Migrate a Tiered Program Group to a Cascade The [tiered affiliate program recipe](/recipes/tiered-affiliate-program) builds tiers by stacking two programs inside a [program group](/documentation/general/what-are-program-groups). Standard affiliates earn one rate, VIP affiliates earn a higher one, and the program group makes sure only one program fires per sale. That shape works. The two programs pay the right rate per affiliate on any tier, including Lite, and the group, an Essentials feature, is what stops a sale that matches both tiers from paying twice. On Siren Pro there is a different model that removes the second program and the promotion churn that comes with it. You bind one program to a [linear-chain collaborator group](/documentation/collaborator-group-structures/linear-chain) and use the [Upline Cascade calculation strategy](/documentation/calculation-strategies/upline-cascade). Tiers stop being separate programs a collaborator gets moved between, and become positions in an ordered chain. This guide walks the conversion. This is the migration that [the tiered affiliate program recipe](/recipes/tiered-affiliate-program) and [what is a cascade](/documentation/general/what-is-a-cascade) point you toward. Read both first if you want the conceptual background before you start. > **Before you migrate, decide whether you should.** This is not a like-for-like conversion, it changes who gets paid. The program-group model pays the seller a higher rate on their own sales. The cascade model pays the people above the seller an override on the seller's production, and the seller earns nothing from the cascade itself. If your tiers are only about giving a collaborator a better rate on their own sales, with nobody earning above them, do not migrate. Stay on the program-group model. Migrate only if your tiers are really about who earns an override on whose production. The trade-off is explained in full below. ## Why switch The program-group model treats a tier as a rate bracket. Each tier is its own program with its own rate, and a collaborator belongs to exactly one tier at a time. Promotion means editing membership: you remove the collaborator from the Standard program and add them to the VIP program. During the handoff a collaborator can briefly appear in both, which is why the program group exists, to keep only one program firing per sale. The cascade model treats a tier as a position. There is one program, bound to one ordered chain of collaborators, and a collaborator's place in the chain determines how rewards flow around them. There is no second program, no program group to keep the tiers mutually exclusive, and no membership churn when someone moves up. You reorder the chain. The trade you are making is worth being clear about. The program-group model pays the seller a different rate depending on their own tier. The cascade model pays the people positioned above the seller, at a score that depends on their distance from the sale. If your "tiers" are really about who earns an override on whose production (a manager earning on a rep's sales, a director earning on a team's production), the cascade is the more direct fit. If your tiers are purely about giving one collaborator a higher rate on their own sales with nobody earning above them, the program-group model is the better match and you should stay on it. See [when to use a cascade](/documentation/general/what-is-a-cascade) for the cases the cascade is built for. ## Before and after Before, you have two programs in a program group. ```json { "programs": { "standard": { "name": "Standard Affiliate", "incentiveType": "saleTransactionPercentage", "incentiveArgs": { "transactionPercent": 10 } }, "vip": { "name": "VIP Affiliate", "incentiveType": "saleTransactionPercentage", "incentiveArgs": { "transactionPercent": 25 } } }, "programGroups": { "affiliateTiers": { "name": "Affiliate Tiers", "sorter": "newestBindingWins", "programs": ["standard", "vip"] } } } ``` After, you have one program bound to a [linear-chain collaborator group](/documentation/collaborator-group-structures/linear-chain), with an [Upline Cascade](/documentation/calculation-strategies/upline-cascade) and per-layer points. The program group is gone, because there is only one program left and nothing to keep mutually exclusive. The two tier rates (10% and 25%) no longer live on two programs. They become per-layer point values on a single cascade, where the layer is the distance up the chain from whoever made the sale. ## How tiers map to chain positions In a linear chain, every member carries an integer `position` in their metadata. Lower numbers sit closer to the top. Position 1 is the head of the chain, position N is the tail. Upline means walking toward lower positions, so the member at position 4 has position 3 as their layer-1 upline, position 2 as layer 2, and position 1 as layer 3. Translate your tiers into an order, top tier first. The collaborator who would have been at the top of your promotion ladder goes at position 1. The next tier down goes at position 2, and so on. A new affiliate joins at the tail, the same way a new affiliate started in the Standard program before. When an Upline Cascade is bound, the per-layer points decide what each position earns when someone below them makes a sale. Layer 1 is the direct upline (one step toward the head), layer 2 is two steps up, and so on, up to a maximum of five layers. You set `pointsAtLayer1` through `pointsAtLayer5`. A layer set to 0, or left unset, stops the cascade, so nothing past that layer is credited. That zero is the lever for "only pay this many layers deep." For example, if you want the immediate upline to earn the most and the layer above that to earn less: - `pointsAtLayer1: 100` - `pointsAtLayer2: 50` - `pointsAtLayer3: 0` A sale by the member at the tail credits their layer-1 upline 100 points and their layer-2 upline 50 points. Layer 3 is 0, so the cascade stops. The member who made the sale earns nothing from the cascade, because the triggering collaborator is never credited by it. Their own payout, if they should have one, comes from a separate calculation strategy (usually [fixed](/documentation/calculation-strategies/fixed)) bound to the same program. ## How per-layer points become payouts The per-layer numbers are scores, not dollars on their own. They feed into whatever incentive structure the program uses to turn scores into payouts, which is the same decision you made when you set the rate on each tiered program. With a fixed-per-incentive structure, each layer's points become a literal payout, so 100 points pays out as a fixed amount you configure. With a [performance-weighted pool](/documentation/distribution-structures/performance-weighted-pool), the scores become weights for splitting a fixed pool, so 100/50/25 becomes a 4:2:1 split of whatever the pool holds. Pick whichever matches how you want the old tier rates to translate. ## Step-by-step conversion 1. Confirm you are on Siren Pro. Linear-chain collaborator groups and cascade calculation strategies are Pro features. The program-group tiered model runs on Essentials, so if you are not on Pro yet, leave the existing setup in place until you upgrade. 2. Create a collaborator group and set its structure to Linear Chain. See [create a collaborator group](/documentation/getting-started/create-a-collaborator-group) for the full walkthrough. 3. Add your collaborators to the group. These are the same WordPress users who were enrolled in your Standard and VIP programs, so no new accounts are created. 4. Drag the members into tier order, top tier first. The top of the list becomes position 1. Siren writes the positions from the visual order when you save, so you do not type position numbers by hand. 5. Pick the single program you will keep. You can reuse the Standard program or the VIP program as the base, since you are about to replace how its reward is calculated. Bind that program to the collaborator group you just created. 6. On the program's edit screen, set the calculation strategy to Upline Cascade. The strategy only appears in the picker when the bound group is a linear chain or parent-child structure, because a flat group has no layers to walk. 7. Set the per-layer points. Translate your old tier rates into `pointsAtLayer1` through `pointsAtLayer5`, leaving the layers you do not need at 0. Configure how scores become payouts (a fixed amount per point, or a pool split) to match the rates you charged before. 8. Keep the seller earning on their own sales. In the old model each seller earned their tier rate on their own sales. The cascade never credits the seller, only the people above them, so without this step sellers stop earning on their own production entirely. Bind a separate calculation to the same program for the triggering collaborator's own payout, using the incentive that matches what they earned before: a [percentage of transaction](/documentation/incentive-structures/percentage-of-transaction) to reproduce a percentage rate, or a [fixed](/documentation/calculation-strategies/fixed) amount for a flat one. Note that this own-sale rate is now uniform across the chain. The per-tier difference in what sellers earned on their own sales does not survive the move to a cascade, which is the trade-off described above. 9. Deactivate the second program and the program group. Once the single program with its cascade is live, the second tier program and the program group that bundled them are no longer needed. Set them to inactive rather than deleting them, so the historical conversions and obligations they credited stay intact as a record of what was already paid. ## What is preserved and what is rebuilt Preserved without changes: - Collaborator accounts. Every collaborator is a WordPress user, and that user account is untouched. You add the same people to the new group. - Tracking [aliases](/documentation/resource-reference/aliases). Referral codes and coupon codes are stored as aliases on the collaborator, not on the program, so published links keep working. - Historical conversions and obligations. Records created by the old programs stay as history. The conversion shows which program credited it, so past earnings remain visible. Rebuilt during the migration: - The two tier programs collapse to one program with a cascade calculation. The rate that used to live on each program becomes a per-layer point value. - The program group is removed. It existed to keep the tier programs mutually exclusive, and with one program there is nothing to keep exclusive. - Promotion changes from a membership edit to a position change. Instead of moving a collaborator from the Standard program to the VIP program, you reorder them within the chain. Removing someone from the middle shifts the positions below them up so the chain stays contiguous. One detail to plan for. Switching an existing collaborator group's structure does not migrate per-member metadata, so if you reuse a group that was previously flat and switch it to Linear Chain, the existing members come across but their `position` is unset until you order them. Set the order explicitly after the switch. If you create a fresh Linear Chain group, as the steps above do, you order members as you add them and this does not apply. ## Related - [What is a cascade](/documentation/general/what-is-a-cascade) for the conceptual background. - [Linear chain structure](/documentation/collaborator-group-structures/linear-chain) for how positions and layers work. - [Upline cascade calculation](/documentation/calculation-strategies/upline-cascade) for the per-layer arguments and a worked example. - [Migration overview](/documentation/migration/overview) for how Siren's architecture differs from other affiliate plugins. ## Migrating from AffiliateWP Source: https://www.sirenaffiliates.com/documentation/migration/affiliatewp Concept mapping, terminology differences, and what to expect when moving from AffiliateWP to Siren. # Migrating from AffiliateWP This guide maps AffiliateWP's concepts, terminology, and data structures to their Siren equivalents. It assumes you've read the [Migration Overview](/documentation/migration/overview), which explains why Siren's architecture works the way it does. This page focuses on the specifics of moving from AffiliateWP. ## Use Beacon for migration assistance Siren doesn't ship with an automated AffiliateWP importer. Migration is a manual concept-mapping exercise, and the supported way to get help with it is [Beacon](/documentation/getting-started/what-is-beacon), Siren's free AI assistant. Describe your current AffiliateWP setup to Beacon (your global rate, per-affiliate overrides, any add-ons like Tiered Affiliate Rates or Allowed Products, your coupon assignments) and ask it to translate the structure into a Siren recipe. For example, "I'm running a 20% global rate with three VIP affiliates at 30%, and I use the Tiered Affiliate Rates add-on to bump affiliates to 25% after $5,000 in sales. Can you build me a recipe that matches this?" Beacon will typically respond with a [program group](/documentation/general/what-are-program-groups) for the tier logic and a [distributor](/documentation/general/what-are-distributors) for the performance threshold. Review the mapping tables below alongside Beacon's output so you understand what each piece is doing. ## Terminology mapping | AffiliateWP Term | Siren Equivalent | Notes | |---|---|---| | Affiliate | [Collaborator](/documentation/general/what-is-a-collaborator) | Broader term, same WordPress user model underneath | | Referral | [Conversion](/documentation/general/what-is-a-conversion) + [Obligation](/documentation/general/what-are-obligations) | Siren separates "what happened" from "what's owed" | | Visit | [Opportunity](/documentation/general/what-is-an-opportunity) | The trackable visitor instance | | (no equivalent) | [Engagement](/documentation/general/what-is-an-engagement) | Per-program credit claim. AffiliateWP doesn't have this because it doesn't have multi-program. | | Payout | [Fulfillment](/documentation/general/what-is-a-fulfillment) (batch) + Payout (individual) | Siren splits the batch from the individual payment records within it | | Creative | Not used | WordPress block editor handles promotional materials. See the [Migration Overview](/documentation/migration/overview) for context. | | Campaign | Not used | Nobody has asked for it | | Coupon | Alias (type: coupon) | See [Aliases](/documentation/resource-reference/aliases) | | Referral URL parameter | Alias (type: tracking) | Existing referral links keep working through aliases | | Global commission rate | [Program](/documentation/general/what-are-programs) | Rates live on programs, not globally or per-affiliate | | Per-affiliate rate override | Separate program enrollment | Create a program with different rates and enroll the collaborator in it | | Referral type (sale/opt-in/lead) | Conversion type (sale/lead/renewal) | | | Direct Link Tracking | Not supported | | | Lifetime Commissions | Roadmapped, not yet available | | | Recurring Referrals | Supported via renewal conversion type | Requires the Renewals feature flag | | Affiliate Groups | [Program Groups](/documentation/general/what-are-program-groups) | The concept is different. See the section below on program groups. | | Smart Commission Rules | Programs | Per-product and per-category filtering are built into programs | | Tiered Affiliate Rates | [Distributor](/documentation/general/what-are-distributors) | Scheduled aggregate bonuses based on performance metrics. See [add-ons and distributors](#add-ons-and-distributors) below. | | Multi-tier / override commissions (add-on) | [Collaborator group](/documentation/general/what-are-collaborator-groups) + [Cascade](/documentation/general/what-is-a-cascade) | Pro tier. Arrange collaborators in a hierarchical group and use an Upline or Downline Cascade to credit each tier by layer. See [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade). | | Sign Up Bonus | [Distributor](/documentation/general/what-are-distributors) or Program | See [add-ons and distributors](#add-ons-and-distributors) below. | | Leaderboard | [Distributor](/documentation/general/what-are-distributors) | Distributors compute rankings as part of their normal operation. | | (no equivalent) | [Distributor](/documentation/general/what-are-distributors) | Scheduled, aggregate reward distribution based on tracked metrics over time. AffiliateWP has no equivalent concept. | | Allowed Products add-on | [Line item filters](/documentation/general/line-item-filters) | Composable filters by category, SKU, product type, or ownership. See [product-level commission control](#transaction-filtering-and-commission-calculations) below. | | Disable referrals (per product) | [Line item filters](/documentation/general/line-item-filters) | Filter approach instead of per-product toggles. | | Per-product rates | [Program](/documentation/general/what-are-programs) with [line item filters](/documentation/general/line-item-filters) | Rates live on programs. Filters control which products qualify. See [transaction filtering](#transaction-filtering-and-commission-calculations) below. | ## Status mapping ### Referral statuses AffiliateWP referral statuses map to a combination of conversion and obligation statuses in Siren. | AffiliateWP Referral Status | Siren Equivalent | |---|---| | pending | Conversion: pending | | unpaid | Obligation: pending (approved, awaiting payout) | | paid | Obligation: complete (included in a fulfillment) | | rejected | Conversion: rejected OR Obligation: rejected | The split between conversion and obligation statuses is why "pending" maps differently than "unpaid." In AffiliateWP, a pending referral hasn't been reviewed yet. An unpaid referral has been approved but not paid. In Siren, the conversion tracks whether the event was legitimate, and the obligation tracks whether the payment has been made. These are separate questions with separate answers. ### Affiliate statuses These map directly. | AffiliateWP Affiliate Status | Siren Collaborator Status | |---|---| | active | active | | inactive | inactive | | pending | pending | | rejected | rejected | ## Architectural differences The [Migration Overview](/documentation/migration/overview) covers Siren's general architecture. This section focuses on the differences that will feel most noticeable coming from AffiliateWP specifically. ### Commission rates live on programs, not affiliates In AffiliateWP, you set a global commission rate in Settings, then override it per affiliate when someone negotiates a better deal. The more affiliates you have with custom rates, the harder it gets to see what's actually happening across your program. Siren puts rates on [programs](/documentation/general/what-are-programs). Each program defines what triggers a reward, how it's calculated, and who's eligible. If you need a different rate for a specific collaborator, you create a program with that rate and enroll them. All the commission logic stays in one place rather than being scattered across individual affiliate profiles. This feels like more work at first. In practice, most AffiliateWP setups have only a handful of distinct rate tiers. Turning those tiers into named programs makes the structure visible and easier to reason about. ### One sale can create multiple payouts AffiliateWP creates one referral per sale. One affiliate gets credit, one commission is calculated, one payout eventually happens. Siren can create multiple [conversions](/documentation/general/what-is-a-conversion) and [obligations](/documentation/general/what-are-obligations) from a single transaction because each program evaluates the sale independently. If a customer clicks an affiliate link and then buys a course created by a content partner, the affiliate might earn a 25% commission and the course creator might earn a 50% royalty. Two obligations from the same sale, through two different programs, without conflict. If you only run a single affiliate program, this works identically to AffiliateWP. The separation only becomes visible when your incentive structures grow beyond one-affiliate-one-commission. ### Program groups are not affiliate groups AffiliateWP's affiliate groups are organizational buckets. You put affiliates into groups to apply bulk rate overrides or manage them more easily. The groups themselves don't have logic. Siren's [program groups](/documentation/general/what-are-program-groups) solve a different problem. A program group is a set of mutually exclusive programs. When a sale could trigger multiple programs in the same group, the group's sorter picks a winner. For example, if you have both a "Standard Affiliate" program and a "VIP Affiliate" program in the same group, the sorter determines which one applies to a given conversion. If you're using AffiliateWP affiliate groups purely for organization, the equivalent in Siren is just enrolling collaborators in the appropriate programs. If you're using groups to manage which rate applies, that logic moves to program groups with sorters. ### No per-affiliate rate overrides This follows from rates living on programs. In AffiliateWP, you might have 50 affiliates at 20% and 3 at 30%. In Siren, you'd create two programs: one at 20% and one at 30%. The three collaborators who earn 30% are enrolled in the higher-rate program instead of (or in addition to) the standard one. This means looking at a program tells you exactly who earns what. You don't have to check each collaborator's profile to find hidden overrides. ### Tracking is more granular An AffiliateWP visit records "someone clicked affiliate X's link." Siren splits this into two records. An [opportunity](/documentation/general/what-is-an-opportunity) records "someone arrived through this referral mechanism." An [engagement](/documentation/general/what-is-an-engagement) records "this collaborator has a credit claim in this specific program." One opportunity can produce multiple engagements if the collaborator is enrolled in multiple programs. For a single-program setup, this distinction is invisible. You'll see opportunities where you used to see visits, and the engagement layer operates in the background. For multi-program setups, the separation is what allows per-program attribution to work. ## Add-ons and distributors Several AffiliateWP add-ons cover use cases that Siren handles through [distributors](/documentation/general/what-are-distributors). AffiliateWP's Tiered Affiliate Rates add-on automatically bumps an affiliate's commission rate when they hit a threshold, like 50 referrals or $5,000 in earnings. The rate change applies going forward to future transactions. In Siren, a distributor can track the same metrics (sales count, total earnings, or anything else) over a period and award a bonus when the distribution triggers. The difference is that AffiliateWP changes the per-transaction rate, while Siren pays an aggregate bonus on a schedule. The result for the collaborator is similar, but Siren's approach is more flexible because you can track any metric, use any schedule, and distribute the reward proportionally, to the top performer, or evenly across everyone who hit the threshold. AffiliateWP's Sign Up Bonus add-on awards a one-time flat payment when a new affiliate registers. In Siren, a distributor can handle this, though for a simple one-time bonus a program may be more straightforward depending on the setup. If you're using AffiliateWP's Recurring Referrals add-on to pay commission on subscription renewals, Siren supports this through the renewal conversion type when the Renewals feature flag is active. This is a per-transaction feature handled by programs, not distributors. But if you want to pay a bonus based on how many renewals a collaborator generated over a month or quarter, a distributor handles that naturally. AffiliateWP's Leaderboard add-on displays a ranked list of top affiliates. Distributors compute exactly this kind of ranking as part of their normal operation, since incentive structures like top score wins and performance weighted pool require calculating who performed best. The display side is separate, but the data is there. ## Transaction filtering and commission calculations AffiliateWP calculates percentage commissions per line item and always uses the post-discount amount. There is no option to calculate on the pre-discount price. Shipping and tax each have a toggle in Settings > Commissions to include or exclude them. Fees (signup fees, subscription fees) are included with no toggle. For product-level control, AffiliateWP offers per-product rates, per-category rates, a "Disable referrals" checkbox to exclude individual products, and a free Allowed Products add-on that provides an allowlist of eligible product IDs. These work through a priority hierarchy where the most specific rate wins (affiliate-specific beats product, beats category, beats global). Siren's [transaction filtering](/documentation/general/line-item-filters) works differently in two ways. First, the commission base itself is modular. Each component of a transaction (line items, discounts, fees, shipping, taxes) is an independent compiler that you enable or disable per program. If you want commissions on the pre-discount amount, don't enable the discounts compiler. If you want subscription signup fees to count, enable the fees compiler. AffiliateWP hardcodes discounts and fees. Siren lets you choose. Second, when line items are enabled, composable filters narrow which products qualify. You can filter by category, SKU, product type (products vs. subscriptions), or collaborator ownership, and filters combine with AND logic. AffiliateWP has category-based rates and product exclusions, but cannot filter by SKU or product type, and cannot combine independent filter dimensions. ### Per-product and per-category rates AffiliateWP lets you set a custom commission rate on individual products and on product categories. If Product A should pay 30% and Product B should pay 15%, you set those rates directly on each product's edit screen. If everything in the "courses" category should pay 25%, you set a category rate. Siren doesn't have per-product or per-category rate fields. Instead, you create separate programs with different rates and use line item filters to control which products each program applies to. A program paying 30% filtered to Product A's SKU and a program paying 15% filtered to Product B's SKU achieves the same result. For category-based rates, a program filtered to the "courses" category with a 25% rate works the same way. The tradeoff is that Siren requires creating a program for each distinct rate, while AffiliateWP lets you set rates inline on product screens. The upside is that Siren's approach keeps all commission logic visible in one place (the programs list) rather than scattered across individual product settings. It also means you can combine the category filter with other dimensions. A program that pays 25% only on subscription products in the "courses" category that are owned by the collaborator is a single program in Siren. In AffiliateWP, there is no way to express that combination because per-product rates, per-category rates, and per-affiliate rates are separate override layers that don't compose. When migrating, audit your per-product and per-category rates and group them by distinct rate. Each group becomes a program with the appropriate rate and filters. Most stores have a small number of distinct rates even if they're applied to many products, so the number of programs stays manageable. ## What migrates These AffiliateWP records have direct Siren equivalents and can be migrated. - Affiliate accounts become collaborator records, linked to the same WordPress user accounts. - Affiliate tracking IDs become [aliases](/documentation/resource-reference/aliases) (type: tracking), so existing referral links continue to resolve. - Referral history becomes conversions (historical) and obligations (fulfilled). - Coupon assignments become aliases (type: coupon). - Payout history becomes fulfillments and payouts (historical records). - Commission rates require creating programs that match your current rate structure. If you have a 20% global rate and a few affiliates at 30%, you'll create two programs. ## What doesn't migrate directly Some AffiliateWP features have no Siren equivalent, either by design or because they haven't been built yet. - Creatives. Siren doesn't have a built-in creative management system. Use WordPress pages or blocks to provide promotional materials to your collaborators. - Campaign data. The campaign label that AffiliateWP attaches to visits and referrals has no equivalent in Siren. - Direct Link Tracking. Not supported. - Lifetime commission customer links. Roadmapped but not yet available. - Per-affiliate rate overrides. These need to be restructured as separate programs with the appropriate rates. - Active browser cookies. Visitors who clicked a referral link before the migration but haven't purchased yet will lose their attribution. This is a narrow window for most sites, but worth considering if you use long cookie durations. ## Database table reference A quick reference for how AffiliateWP's database tables map to Siren's tables. | AffiliateWP Table | Siren Equivalent | |---|---| | affiliate_wp_affiliates | Collaborators table + Aliases table | | affiliate_wp_referrals | Conversions table + Obligations table | | affiliate_wp_visits | Opportunities table | | affiliate_wp_payouts | Fulfillments table + Payouts table | | affiliate_wp_creatives | No equivalent | | affiliate_wp_customers | Tracked through opportunities and engagements | ## Migrating from Easy Affiliate Source: https://www.sirenaffiliates.com/documentation/migration/easy-affiliate Concept mapping, terminology differences, and what to expect when moving from Easy Affiliate to Siren. # Migrating from Easy Affiliate This guide covers what to expect when moving from Easy Affiliate (formerly Affiliate Royale) to Siren. If you haven't read the [Migration Overview](/documentation/migration/overview), start there. It explains the structural differences that apply regardless of which plugin you're coming from. ## Use Beacon for migration assistance There's no automated Easy Affiliate importer. Migration is a manual exercise where you describe your current setup and rebuild the equivalent structure in Siren, and the quickest way through it is with [Beacon](/documentation/getting-started/what-is-beacon), Siren's free AI assistant. If you're using Easy Affiliate's Commission Rules add-on with a rule builder full of product-specific rates or threshold bonuses, describe the rules to Beacon and ask it to translate them into a Siren recipe. A prompt like "I've got a 25% global rate, a rule that bumps to 30% for products in the courses category, and a sign-up bonus of $50 for new affiliates. Build me a matching Siren recipe" gets you most of the way there. If your current setup uses Commission Levels (Easy Affiliate's tiered commission feature), tell Beacon that upfront. On the Pro tier these map to a [collaborator group](/documentation/general/what-are-collaborator-groups) with an Upline or Downline [Cascade](/documentation/general/what-is-a-cascade), and Beacon can help you lay out the chain and per-layer points. If you're on a lower tier or would rather not model a hierarchy, Beacon can also help you think through what to reward instead: per-contribution payouts, content royalties, or standalone bonus programs that capture the intent without modeling a full hierarchy. ## Terminology mapping The biggest source of confusion when switching is that both plugins use the word "transaction" to mean completely different things. Read the table below carefully, and pay special attention to the Transaction row. | Easy Affiliate Term | Siren Equivalent | Notes | |---|---|---| | Affiliate | [Collaborator](/documentation/general/what-is-a-collaborator) | Same concept, broader label. Every collaborator is a WordPress user. | | Transaction | [Conversion](/documentation/general/what-is-a-conversion) + [Obligation](/documentation/general/what-are-obligations) | See the warning below. Easy Affiliate's "transaction" is the commission record. Siren's "transaction" is the purchase record. These are not the same thing. | | Click | [Opportunity](/documentation/general/what-is-an-opportunity) | A tracked visit from a referral link. | | (no equivalent) | [Engagement](/documentation/general/what-is-an-engagement) | A credit claim against a specific program. New concept in Siren with no Easy Affiliate counterpart. | | Commission (the money earned) | [Obligation](/documentation/general/what-are-obligations) | The record of what you owe a collaborator. | | Commission Level | [Collaborator group](/documentation/general/what-are-collaborator-groups) + [Cascade](/documentation/general/what-is-a-cascade) | Pro tier. Each commission level maps to a layer in a hierarchical group, credited by an Upline or Downline Cascade. See the section on commission levels below. | | Pay Affiliates | [Fulfillment](/documentation/general/what-is-a-fulfillment) | The payout process that settles obligations. | | Link | [Alias](/documentation/resource-reference/aliases) (type: tracking) | Referral codes and URLs are stored as aliases. | | Creative | Not used | Siren relies on WordPress blocks for promotional materials. | | Global commission rate | [Program](/documentation/general/what-are-programs) | Rates live on programs, not in global settings. | | Per-user rate override | Separate program enrollment | Instead of overriding a rate for one affiliate, you enroll them in a different program with the rate you want. | | Commission Rules | [Distributor](/documentation/general/what-are-distributors) or [Program](/documentation/general/what-are-programs) | Threshold-based bonuses map to distributors. Product-specific rates map to programs with line item filters. See [add-ons and distributors](#add-ons-and-distributors) below. | | (no equivalent) | [Distributor](/documentation/general/what-are-distributors) | Scheduled, aggregate reward distribution based on tracked metrics over time. Easy Affiliate has no equivalent concept. | | (no equivalent) | [Line item filters](/documentation/general/line-item-filters) | Composable filters by category, SKU, product type, or ownership. See [product-level commission control](#transaction-filtering-and-commission-calculations) below. | ### The "Transaction" confusion This is the single most important thing to understand when migrating. In Easy Affiliate, a "transaction" is the commission record. It tracks who earned what from which sale. When you look at the Transactions screen in Easy Affiliate, you see a list of commissions. In Siren, a [transaction](/documentation/general/what-are-transactions) is the purchase record. It contains line items, totals, and payment details. It represents what the customer bought, not what the collaborator earned. Siren's equivalent of Easy Affiliate's transaction is a conversion paired with an obligation. The conversion records the attribution (this sale was credited to this collaborator through this program). The obligation records the debt (you owe this collaborator this amount). Together, they cover the same ground as a single Easy Affiliate transaction record. When reading Siren's documentation or looking at database tables, don't let the shared word trip you up. Easy Affiliate transaction = Siren conversion + obligation. Siren transaction = the purchase itself. ## Status mapping Easy Affiliate transaction statuses map to Siren across two record types. | Easy Affiliate Status | Siren Equivalent | Where it lives | |---|---|---| | Pending | Pending | On the conversion | | Complete | Complete | On the obligation | | (paid via Pay Affiliates) | Complete, within a fulfillment | The obligation is marked complete and grouped into a [fulfillment](/documentation/general/what-is-a-fulfillment) record | Easy Affiliate combines the commission status and payout status into one field on the transaction. Siren separates them. The conversion tracks whether the attribution is valid. The obligation tracks whether the money has been paid. This means you can approve a conversion (yes, this sale counts) without immediately marking it as paid. ## Architectural differences ### Commission Levels vs. cascades Easy Affiliate supports tiered commission structures. Level 1 is the direct referral commission. Level 2 goes to the person above the Level 1 collaborator in the hierarchy. Level 3 goes up another tier. This is a tiered structure where each level earns an override on the level below, the way a sales manager earns an override on their reps and a regional director earns a smaller override above that. On the Pro tier, Siren maps commission levels directly. You arrange affiliates into a hierarchical [collaborator group](/documentation/general/what-are-collaborator-groups) (a linear chain or a parent-child tree), bind the group to a program, and add an Upline [Cascade](/documentation/general/what-is-a-cascade) calculation strategy. Each Easy Affiliate level becomes a layer in the chain. Per-layer points (`pointsAtLayer1` through `pointsAtLayer5`, up to five layers) control how much each level earns, and the triggering affiliate is never credited by the cascade itself. Setting a layer to 0 stops the cascade there, so you decide how many levels pay out. See [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade) for the conversion path. If a hierarchy isn't what you actually need, Siren also lets you use independent [programs](/documentation/general/what-are-programs) to reward different kinds of contributions. If you want to pay someone a one-time bonus for bringing in a new partner, you create a program with its own rules. If you want to pay content creators a royalty, you create a royalty program. Each program defines what triggers a reward and how much it pays, independently of other programs. The question to ask is what each person is actually contributing. A team lead who should earn an ongoing override on the sales of the people they oversee fits a cascade. A one-time bonus for bringing in a partner fits a standalone program. ### Commission rates live on programs, not affiliates Easy Affiliate uses a global commission rate set in the plugin settings. When a specific affiliate needs a different rate, you override it on their profile. Siren handles this differently. Commission rates are defined on programs. If you need different rates for different situations, you create separate programs. An affiliate who negotiated a 30% rate gets enrolled in a program that pays 30%. There is no global rate to override because every program defines its own rate explicitly. This makes it easier to see what rate applies where. Instead of checking the global settings and then checking whether each affiliate has an override, you look at which program they are enrolled in. ### Engagements are new Easy Affiliate tracks clicks. A click records that someone arrived through an affiliate's link. Siren tracks [opportunities](/documentation/general/what-is-an-opportunity) (similar to clicks) and [engagements](/documentation/general/what-is-an-engagement) (new concept). An engagement is a credit claim against a specific program. If a collaborator is enrolled in three programs, one opportunity can spawn three engagements, one per program. [Program groups](/documentation/general/what-are-program-groups) then determine which engagement wins when a conversion happens. You don't need to migrate engagement data because Easy Affiliate doesn't have it. But understanding engagements helps you make sense of Siren's attribution pipeline after migration. ## Add-ons and distributors Easy Affiliate's Commission Rules add-on lets you change commission rates based on thresholds. You can set rules like "after 50 sales, increase the rate to 30%" or "for this specific product, pay 15% instead of the default." The threshold-based rules adjust the per-transaction rate going forward. Siren handles the threshold case through [distributors](/documentation/general/what-are-distributors). A distributor tracks performance over a period and awards a bonus when the distribution triggers. If you want to reward a collaborator for hitting 50 sales in a month, a distributor can track that metric and pay a bonus at month's end. The product-specific rate case is handled differently: Siren programs support line item filters that control which products are eligible for commissions, so you create a program with the rate you want and filter it to the right products. Easy Affiliate doesn't have a dedicated performance bonus, leaderboard, or milestone reward feature at any tier. If you've been working around this limitation, Siren's distributor system gives you a proper way to handle scheduled, aggregate rewards based on any combination of tracked metrics. ## Transaction filtering and commission calculations Easy Affiliate calculates commissions on the transaction subtotal after discounts, with shipping and taxes always excluded. None of these are configurable. The commission base is the post-discount, pre-tax, pre-shipping subtotal, and that's the only option. The Commission Rules add-on provides a rule builder for product-level control. You can match on product ID, product name, source integration, or coupon code, and set a commission rate for matching items. Rules support AND/OR logic within a single rule. However, Commission Rules cannot filter by product category, SKU, or product type. If you want a rate for all products in a category, you need to list each product ID individually. The commission model is also transaction-oriented rather than per-line-item, so multi-product orders don't get granular per-item calculations. Siren's [transaction filtering](/documentation/general/line-item-filters) gives you control at both levels. For the commission base, each component (line items, discounts, fees, shipping, taxes) is an independent compiler that you enable or disable per program. Easy Affiliate hardcodes all of these. Siren lets you choose whether discounts are subtracted, whether fees count, and whether shipping or taxes are included. For product-level control, Siren's line item filters narrow which items are eligible using category, SKU, product type, or collaborator ownership, and filters combine with AND logic. The commission rate comes from the program, and the filters determine which items that rate applies to. ### Per-product rates Easy Affiliate's Commission Rules add-on handles per-product rates through a rule builder. You create a rule matching a product by ID or name and set a commission rate. If you have 10 products with custom rates, you create 10 rules. In Siren, each distinct rate becomes a program with line item filters targeting the relevant products by SKU or category. A 30% program filtered to one set of SKUs and a 15% program filtered to another set achieves the same outcome. Easy Affiliate's rule builder can match on product ID and product name, but not on category. Siren can filter by category, which means adding a new product to a category automatically includes it without creating a new rule. The tradeoff is that Easy Affiliate's rules are defined in a single settings screen, while Siren's approach requires creating separate programs. The upside is that each program's scope is explicit and composable with other filter dimensions. ## What migrates and what doesn't | Easy Affiliate Data | Migrates to Siren? | Details | |---|---|---| | Affiliates | Yes | Map to collaborators. User accounts already exist in WordPress. | | Transactions (their term) | Yes | Map to conversions + obligations. Historical records import as completed. | | Clicks | Yes | Map to opportunities. | | Tracking links | Yes | Map to [aliases](/documentation/resource-reference/aliases) (type: tracking). Existing referral codes are preserved so published links keep working. | | Commission Levels | Rebuild on Pro | Recreate the tiers as a [collaborator group](/documentation/general/what-are-collaborator-groups) with an Upline or Downline [Cascade](/documentation/general/what-is-a-cascade). There is no automated import, so the hierarchy and per-layer points are set by hand. On lower tiers, restructure as separate programs instead. | | Creatives | No | Use WordPress blocks to build promotional pages. There is no built-in creative library in Siren. | | Legacy Affiliate Royale data | Same mapping | If your site still uses the older `wafp_` prefixed database tables from Affiliate Royale, the same mapping applies. The data structure is essentially the same as Easy Affiliate's `esaf_` tables. | ## Database table reference If you are writing a custom migration script or inspecting the data directly, here is how the Easy Affiliate database tables map to Siren concepts. Easy Affiliate uses the `esaf_` table prefix. Older installations that were originally Affiliate Royale may still use the `wafp_` prefix. The table structures are the same either way. | Easy Affiliate Table | Siren Equivalent | Notes | |---|---|---| | `esaf_transactions` (or `wafp_transactions`) | Conversions + Obligations | Each Easy Affiliate transaction becomes a conversion record (the attribution) and an obligation record (the amount owed). | | `esaf_clicks` (or `wafp_clicks`) | Opportunities | Click records map directly to opportunity records. | | `esaf_affiliates` (or `wafp_affiliates`) | Collaborators + Aliases | The affiliate record becomes a collaborator. The affiliate's referral slug becomes a tracking alias. | There is no Siren table that maps one-to-one with Easy Affiliate's transaction table because Siren splits the concept into two separate records. A migration script needs to create both a conversion and an obligation for each Easy Affiliate transaction. ## Next steps After reviewing this mapping, read the [Migration Overview](/documentation/migration/overview) if you haven't already. It covers the broader architectural differences, what to expect during the transition, and how Siren's program-based approach works in practice. The Siren concepts referenced in this guide are covered in detail in the User Guide under Core Concepts, including [programs](/documentation/general/what-are-programs), [conversions](/documentation/general/what-is-a-conversion), [obligations](/documentation/general/what-are-obligations), [opportunities](/documentation/general/what-is-an-opportunity), [engagements](/documentation/general/what-is-an-engagement), [fulfillments](/documentation/general/what-is-a-fulfillment), and [program groups](/documentation/general/what-are-program-groups). ## Migrating from SliceWP Source: https://www.sirenaffiliates.com/documentation/migration/slicewp Concept mapping, terminology differences, and what to expect when moving from SliceWP to Siren. # Migrating from SliceWP SliceWP and Siren both manage affiliate partnerships in WordPress, but they're built around different ideas. SliceWP is a straightforward affiliate tracker: affiliates share links, customers click them, commissions are recorded. Siren is a broader incentive management system that can do affiliate tracking but also handles royalties, revenue shares, bonuses, and other reward structures. This guide maps SliceWP's concepts to Siren's, explains where the two systems differ, and covers what to expect during a migration. For the architectural reasoning behind Siren's approach, see the [migration overview](/documentation/migration/overview). ## Use Beacon for migration assistance There's no automated SliceWP importer today. The supported way to migrate is to describe your current setup to [Beacon](/documentation/getting-started/what-is-beacon), Siren's free AI assistant, and let it suggest the matching recipe. If you're using SliceWP's Performance Bonuses add-on, describe the rules to Beacon and it'll build you a [distributor](/documentation/general/what-are-distributors) that captures the same behavior. If you're using Product Commission Rates or Product Revenue Share, describe the rate breakdown and Beacon will translate it into a program with the right line item filters. A good starting prompt is something like "I'm coming from SliceWP. My setup is a 20% global rate, a Performance Bonus that pays $100 to the top referrer each month, and Product Revenue Share on a handful of courses. Build me a recipe that matches this." Beacon will walk you through any gaps and flag anything that doesn't have a direct equivalent. ## Terminology mapping Most of what you know from SliceWP has a counterpart in Siren, though some concepts are split into multiple parts and a few are new. | SliceWP Term | Siren Equivalent | Notes | |---|---|---| | Affiliate | [Collaborator](/documentation/general/what-is-a-collaborator) | Same role, broader label. Collaborators can be affiliates, creators, salespeople, or anyone earning incentives. | | Commission | [Conversion](/documentation/general/what-is-a-conversion) + [Obligation](/documentation/general/what-are-obligations) | SliceWP combines "what happened" and "what's owed" into one commission record. Siren separates them. More on this below. | | Visit | [Opportunity](/documentation/general/what-is-an-opportunity) | A record of a tracked visitor arriving through a referral mechanism. | | (no equivalent) | [Engagement](/documentation/general/what-is-an-engagement) | A per-program credit claim. When a visitor arrives and the collaborator is enrolled in multiple programs, each program gets its own engagement. SliceWP doesn't have this because it doesn't have multi-program attribution. | | Payout | [Fulfillment](/documentation/general/what-is-a-fulfillment) | The batch operation that groups payments together. | | Payment | Payout | An individual payment to a collaborator within a fulfillment. | | Customer | Tracked through [opportunities](/documentation/general/what-is-an-opportunity) | Siren doesn't maintain a separate customer table. Customer attribution is tracked through opportunities and engagements instead. | | Coupon (Pro add-on) | [Alias](/documentation/resource-reference/aliases) (type: coupon) | Coupon-based tracking in Siren uses the alias system rather than a separate feature. | | Affiliate referral link | [Alias](/documentation/resource-reference/aliases) (type: tracking) | Referral codes are stored as aliases. The alias system preserves history, so old codes keep working even after changes. | | Global commission rate | [Program](/documentation/general/what-are-programs) | SliceWP uses one global rate with per-affiliate overrides. Siren puts rates on programs instead. | | Commission Items | Transaction details | Line-item level data on conversions and obligations. | | Multi-tier / override commissions | [Collaborator group](/documentation/general/what-are-collaborator-groups) + [Cascade](/documentation/general/what-is-a-cascade) | On Pro, arrange collaborators in a hierarchical group and credit each layer of the hierarchy with an Upline or Downline Cascade. See [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade). | | Cross-site Tracking | Not used | Siren tracks within a single WordPress installation. | | Performance Bonuses | [Distributor](/documentation/general/what-are-distributors) | Scheduled aggregate bonuses based on tracked metrics. See [add-ons and distributors](#add-ons-and-distributors) below. | | Product Revenue Share | [Program](/documentation/general/what-are-programs) with line item filters | A program with collaborator-owned product filtering. See [add-ons and distributors](#add-ons-and-distributors) below. | | (no equivalent) | [Distributor](/documentation/general/what-are-distributors) | Scheduled, aggregate reward distribution based on tracked metrics over time. SliceWP's Performance Bonuses cover part of this, but distributors are more flexible. | | Product Commission Rates add-on | [Line item filters](/documentation/general/line-item-filters) | Composable filters by category, SKU, product type, or ownership. See [product-level commission control](#transaction-filtering-and-commission-calculations) below. | ## Status mapping ### Commission statuses SliceWP's commission statuses map to different fields in Siren because Siren separates conversions from obligations. | SliceWP Status | Siren Equivalent | Which record | |---|---|---| | pending | pending | Conversion | | unpaid | pending | Obligation | | paid | complete | Obligation | | rejected | rejected | Conversion or Obligation (depending on when rejection happens) | In SliceWP, a commission moves through pending, unpaid, and paid as a single record. In Siren, the conversion tracks whether the sale itself is valid, and the obligation tracks whether the collaborator has been paid. A conversion can be approved while the obligation is still pending payment. They move through their lifecycles independently. ### Affiliate statuses | SliceWP Status | Siren Status | |---|---| | active | active | | pending | pending | | rejected | rejected | These map directly. No surprises here. ## Architectural differences ### Commission rates live on programs, not affiliates SliceWP uses a global commission rate. You set it once, and every affiliate earns that rate unless you override it on their individual affiliate profile. The more affiliates with custom rates you have, the harder it gets to see what's actually configured across your system. Siren puts commission rates on [programs](/documentation/general/what-are-programs). A program defines the rate, what triggers the reward, and who is eligible. If you need two different rates, you create two programs. Each program's rules are self-contained and visible in one place rather than scattered across individual affiliate profiles. This means that what was "one affiliate program with a bunch of overrides" in SliceWP often becomes "a few programs with clear, distinct rules" in Siren. ### Tiered structures: cascades or separate programs SliceWP supports tiered override commissions through an add-on, where someone earns a percentage on the sales produced by the collaborators beneath them in a hierarchy. A sales rep closes a deal, their team lead earns an override on it, and a regional director earns a smaller override above that. On the Pro tier, Siren maps this directly. You arrange collaborators into a [collaborator group](/documentation/general/what-are-collaborator-groups) with a hierarchical structure (a linear chain or a parent-child tree), bind the group to a program, and add a [Cascade](/documentation/general/what-is-a-cascade) calculation strategy. A Downline Cascade credits the collaborators below them, layer by layer, when a sale is attributed to them, and an Upline Cascade credits the collaborators above them. Per-layer points (`pointsAtLayer1` through `pointsAtLayer5`, up to five layers) control how much each layer earns, and the triggering collaborator is never credited by the cascade itself. See [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade) for the conversion path. If you'd rather not model a hierarchy at all, you can also use independent [programs](/documentation/general/what-are-programs) for different types of contributions. Instead of a tiered override, you might create a separate bonus program that rewards specific contributions alongside your main program. Each program has its own rules, and they don't interfere with each other. ### A commission is a merged record This is the most important structural difference. In SliceWP, when a sale happens, one commission record is created. That record contains both the event (a sale was attributed to this affiliate) and the financial obligation (you owe this affiliate this amount). The commission tracks the sale and the payment in a single row. Siren splits these into separate records. A [conversion](/documentation/general/what-is-a-conversion) records that something happened and was attributed to a collaborator through a program. An [obligation](/documentation/general/what-are-obligations) records what's owed as a result. They're linked, but they have their own statuses and lifecycles. Why does this matter? Because in Siren, one sale can create multiple conversions and obligations across different programs. If a customer clicks an affiliate link and buys a course, the referring affiliate might earn a 20% commission through the affiliate program while the course creator earns a 50% royalty through a creator program. Two obligations from the same transaction, tracked independently. SliceWP can't do this because its commission model assumes one sale equals one record. If you only ever run a single affiliate program, this separation is invisible. Everything works the same way. But it's the reason Siren can handle more-complex incentive structures without workarounds. ### No separate customer tracking SliceWP maintains a customers table that links customers to the affiliate who referred them. This makes it easy to look up "which affiliate brought in this customer." Siren tracks the same attribution data, but through [opportunities](/documentation/general/what-is-an-opportunity) and [engagements](/documentation/general/what-is-an-engagement) rather than a dedicated customer entity. When a visitor arrives through a referral mechanism and later makes a purchase, the attribution chain connects the collaborator to the transaction. You can trace who referred whom, but there isn't a standalone customer record to query. For most use cases this works the same way. If you rely heavily on customer-level reporting in SliceWP, talk through your needs before migrating to make sure the attribution data in Siren gives you what you need. ## Add-ons and distributors SliceWP includes a Performance Bonuses add-on that awards bonuses when affiliates reach targets. You can set targets based on referred order count, referred order totals, or commissions earned, and pay out on a one-time, monthly, quarterly, or yearly basis. Siren handles this through [distributors](/documentation/general/what-are-distributors), which are a more-flexible version of the same idea. A distributor tracks collaborator performance over a period using any combination of metric events (sales, course completions, blog visits, coupon uses, and more). When the distribution triggers on schedule, the reward can go to the top performer, be split proportionally by score, or be divided evenly among everyone who participated. SliceWP's Performance Bonuses are limited to three metrics, four schedule options, and flat bonus amounts. Siren distributors support arbitrary metrics, any schedule, and multiple distribution strategies. If you're using SliceWP's Recurring Commissions add-on, Siren handles per-renewal commissions through the renewal conversion type in programs, not through distributors. But if you want to bonus collaborators based on how many renewals they generated over a period, a distributor handles that. SliceWP's Product Revenue Share add-on links affiliates to specific products so they earn on every sale regardless of referral. In Siren, this is a program with line item filters set to collaborator-owned products. It works the same way but uses the standard program system rather than a separate feature. ## Transaction filtering and commission calculations SliceWP calculates commissions on the post-discount cart total. There is no option to calculate on the pre-discount amount. Shipping and tax each have toggles in the settings to include or exclude them. Fees are not addressed in the documentation. Without the Product Commission Rates add-on, SliceWP calculates percentage commissions from the cart's total amount rather than per line item. The add-on adds per-product and per-category rates, and with it enabled, commissions are calculated per product and summed. You can also disable commissions on individual products through a checkbox on the product edit screen. Siren's [transaction filtering](/documentation/general/line-item-filters) gives you more control at both levels. For the commission base, each component (line items, discounts, fees, shipping, taxes) is an independent compiler that you enable or disable per program. SliceWP hardcodes discounts as always subtracted. Siren lets you choose whether discounts factor in. SliceWP doesn't address fees at all. Siren has a fees compiler you can toggle. For product-level control, Siren's line item filters let you stack category, SKU, product type, and collaborator ownership filters with AND logic. SliceWP's Product Commission Rates add-on provides per-product and per-category control, but cannot filter by SKU or product type, and cannot combine independent filter conditions. ### Per-product and per-category rates With the Product Commission Rates add-on, SliceWP lets you set a custom rate on individual product edit screens and on product categories. If Product A should pay 30% and Product B should pay 15%, you set those rates directly. In Siren, you create a program for each distinct rate and use line item filters to target the products. A 30% program filtered to Product A's SKU and a 15% program filtered to Product B's SKU achieves the same outcome. For categories, a program filtered to a specific category with the desired rate works the same way. This is more setup than SliceWP's inline rate fields, but it scales better. Adding a new product to an eligible category automatically includes it in the program without touching the product's settings. And because filters compose, you can express things like "25% on subscriptions in the premium category" as a single program rather than setting rates on each subscription product individually. ## What migrates | SliceWP Data | Siren Destination | Notes | |---|---|---| | Affiliates | Collaborators | User accounts likely already exist in WordPress. The migration creates collaborator records and enrolls them in the appropriate programs. | | Commissions | Conversions + Obligations | Historical records. Imported as completed conversions and fulfilled obligations to preserve earning history. | | Visits | Opportunities | Historical records. Imported as completed opportunities for reporting continuity. | | Payments | Payouts | Historical payment records within fulfillments. | | Tracking codes | [Aliases](/documentation/resource-reference/aliases) | Existing referral codes are stored as aliases so published links continue to work. | | Tiered override relationships | Rebuild as a collaborator group | On Pro, recreate the hierarchy as a [collaborator group](/documentation/general/what-are-collaborator-groups) with an Upline or Downline [Cascade](/documentation/general/what-is-a-cascade). There is no automated import, so the chain or tree is re-established by hand. On Plus or below, restructure the earning logic as separate programs instead. | | Cross-site tracking config | Does not migrate | Siren operates within a single WordPress installation. | | Customer records | Not directly migrated | Customer attribution is tracked through opportunities and engagements. The data is preserved in the attribution chain, not as a standalone table. | Active browser cookies from SliceWP won't transfer. If a customer clicked a referral link before the migration but hasn't purchased yet, that attribution will be lost unless you handle it manually. For most sites this is a narrow window, but it's worth noting if you have long cookie durations configured. ## Database table reference If you're writing a custom migration script or inspecting data directly, this shows where SliceWP's tables map to in Siren. | SliceWP Table | Siren Destination | |---|---| | `slicewp_affiliates` | `collaborators` + `aliases` | | `slicewp_commissions` | `conversions` + `obligations` | | `slicewp_visits` | `opportunities` | | `slicewp_payouts` | `fulfillments` | | `slicewp_payments` | `payouts` | | `slicewp_customers` | Tracked through `opportunities` and `engagements` | The `slicewp_affiliates` table splits into two Siren tables because the affiliate's identity (collaborator record linked to a WordPress user) and the affiliate's tracking code (alias record) are stored separately in Siren. Similarly, `slicewp_commissions` splits into conversions and obligations because Siren separates attribution from financial records. ## Migrating from Solid Affiliate Source: https://www.sirenaffiliates.com/documentation/migration/solid-affiliate Concept mapping, terminology differences, and what to expect when moving from Solid Affiliate to Siren. # Migrating from Solid Affiliate This guide covers the terminology differences, status mappings, and architectural gaps between Solid Affiliate and Siren. If you haven't already, read the [migration overview](/documentation/migration/overview) for the broader context on how Siren's architecture differs from traditional affiliate plugins. Solid Affiliate and Siren share a lot of the same goals, but they organize things differently. The biggest adjustment is that Siren moves commission rules from affiliates to [programs](/documentation/general/what-are-programs), and splits the referral record into separate [conversion](/documentation/general/what-is-a-conversion) and [obligation](/documentation/general/what-are-obligations) records. Most of the other differences follow from that. ## Use Beacon for migration assistance Siren doesn't include an automated Solid Affiliate importer. Migration is a manual concept-mapping process, and the fastest way to get through it is with [Beacon](/documentation/getting-started/what-is-beacon), Siren's free AI assistant. Solid Affiliate's rate hierarchy (affiliate group, per-product, per-category, affiliate-product pairings) flattens into Siren programs with line item filters, but the translation isn't always obvious. Describe your current setup to Beacon and ask it to build you a recipe. A prompt like "I'm migrating from Solid Affiliate. I have a Standard group at 20%, a Premium group at 30%, per-product rates on three specific SKUs, and I pay everything out through Bulk Payouts monthly. Build me a matching Siren recipe" is usually enough for Beacon to sketch out the right programs, group rules, and fulfillment schedule. If you're using landing pages or store credit payouts, mention those so Beacon can flag them as features that don't have a direct Siren equivalent. ## Terminology mapping | Solid Affiliate | Siren | Notes | | --- | --- | --- | | Affiliate | Collaborator | Both are WordPress user accounts. Siren uses "collaborator" because the system handles more than affiliate marketing. | | Referral | Conversion + Obligation | A Solid Affiliate referral combines "this sale was attributed" with "this amount is owed." Siren separates these into two records. The conversion tracks attribution. The obligation tracks the debt. | | Visit | Opportunity | Both record a click on a referral link. In Siren, an opportunity can spawn multiple [engagements](/documentation/general/what-is-an-engagement) across programs. | | (no equivalent) | Engagement | A credit claim against a specific program. Solid Affiliate doesn't have this concept because it doesn't have multi-program attribution. | | Payout | Payout | A payment to an individual collaborator. Same concept. | | Bulk Payout | Fulfillment | Solid Affiliate batches payouts into a "bulk payout." Siren calls the batch a fulfillment, and the individual payments within it are payouts. | | Creative | Not used | Siren doesn't have a built-in creative library. Use WordPress pages or a custom post type if you need one. | | Affiliate Group | Program | See the important note below. Solid Affiliate groups are rate-management shortcuts. Siren programs are the structural equivalent, not program groups. | | Affiliate Tag | Not used | Siren doesn't have a tagging system for collaborators. | | Coupon | Alias (type: coupon) | In Siren, a coupon code is an [alias](/documentation/resource-reference/aliases) with the type set to "coupon." | | Landing Page | Not used | Siren attributes through referral links and coupon codes, not per-collaborator landing pages. | | Linked Customer | Not used | Lifetime commissions (permanently linking a customer to a collaborator) are on the roadmap but not available yet. | | Store Credit | Not used | Siren payouts are monetary. Store credit as a payout method isn't supported. | | Revenue Sharing | Program | In Siren, revenue sharing is just a program with the appropriate incentive type and line item filters. It's not a separate feature. | | Custom Affiliate Fields | Not used | WordPress user meta is available for storing arbitrary data on collaborator accounts. | | Sign-up Bonus | [Distributor](/documentation/general/what-are-distributors) or [Program](/documentation/general/what-are-programs) | See [add-ons and distributors](#add-ons-and-distributors) below. | | (no equivalent) | [Distributor](/documentation/general/what-are-distributors) | Scheduled, aggregate reward distribution based on tracked metrics over time. Solid Affiliate has no equivalent concept. See [add-ons and distributors](#add-ons-and-distributors) below. | | Per-product rates / Disable Referrals | [Line item filters](/documentation/general/line-item-filters) | Composable filters by category, SKU, product type, or ownership. See [product-level commission control](#transaction-filtering-and-commission-calculations) below. | | Multi-tier / override commissions (third-party add-on) | [Collaborator group](/documentation/general/what-are-collaborator-groups) + [Cascade](/documentation/general/what-is-a-cascade) | Pro tier. Arrange your sales team or partner hierarchy in a collaborator group and credit each tier by layer with an Upline or Downline Cascade. See [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade). | ### Affiliate Groups are not Program Groups This is the single most confusing mapping between the two systems, so it's worth calling out explicitly. In Solid Affiliate, an affiliate group is a way to apply the same commission rate to a set of affiliates at once. You might have a "Standard" group at 20% and a "Premium" group at 30%. The group is a rate-management shortcut. In Siren, rates live on [programs](/documentation/general/what-are-programs). If you have three Solid Affiliate groups with different rates, the equivalent in Siren is three programs, each with its own rate, with the appropriate collaborators enrolled in each. Siren's [program groups](/documentation/general/what-are-program-groups) are a completely separate concept. A program group makes a set of programs mutually exclusive so that only one fires per conversion, using rules like "newest engagement wins" or "oldest engagement wins" to break ties. Program groups solve an attribution problem, not a rate-management problem. When you see "Affiliate Group" in Solid Affiliate, think "Program" in Siren. Do not think "Program Group." ## Status mapping ### Referral statuses Solid Affiliate tracks referral statuses that combine attribution and payment state. Siren splits these across conversions and obligations. | Solid Affiliate | Siren | Where it lives | | --- | --- | --- | | Draft | pending | Conversion status | | Unpaid | pending | Obligation status (conversion is approved) | | Paid | complete | Obligation status | | Rejected | rejected | Conversion status | In Solid Affiliate, a referral moves from Draft to Unpaid to Paid (or Rejected). In Siren, the conversion is first approved or rejected, and then the obligation moves from pending to complete when the collaborator is paid. The two-record model gives you more visibility into where things stand, but the overall lifecycle is the same. ### Affiliate statuses | Solid Affiliate | Siren | | --- | --- | | Approved | active | | Pending | pending | | Rejected | rejected | These map directly. No surprises here. ### Bulk payout statuses | Solid Affiliate | Siren | | --- | --- | | Processing | processing | | Success | complete | | Fail | failed | Solid Affiliate's bulk payout statuses map to Siren's fulfillment statuses. The individual payouts within a fulfillment also have their own statuses. ## Architectural differences ### Commission rates live on programs, not affiliates Solid Affiliate lets you set per-affiliate rates and per-group rates. The affiliate (or their group) determines what they earn. Siren inverts this. Rates are defined on programs, and collaborators earn based on which programs they're enrolled in. If you need a collaborator to earn a different rate, you enroll them in a different program rather than overriding their personal rate. This is the same shift that applies when migrating from any other affiliate plugin. The [migration overview](/documentation/migration/overview) covers the reasoning in detail. ### Landing pages Solid Affiliate has personalized per-affiliate landing pages that track attribution without requiring referral links. When a visitor lands on an affiliate's custom URL, the system attributes that visit to the affiliate automatically. Siren doesn't have this feature. Attribution happens through referral links (tracking aliases) and coupon codes. If you rely on personalized landing pages for specific affiliates, you'll need to switch those collaborators to standard referral links. ### Store credit Solid Affiliate can pay affiliates via WooCommerce store credit. This is useful when affiliates are also customers who spend money on your site. Siren's fulfillment system handles monetary payouts only. If you're currently paying affiliates in store credit, you'll need to switch to a monetary payout method or handle store credit outside of Siren. ### Revenue sharing Solid Affiliate has a dedicated "Revenue Sharing" feature for setting up product-level partnerships where different parties earn from different products. In Siren, this is just a program. Create a program with the right incentive type and line item filters, and you have revenue sharing. It's not a separate feature because Siren's program system already handles it natively. This is actually simpler in Siren. Instead of learning a separate revenue sharing configuration, you use the same program setup you already know. ## Add-ons and distributors Solid Affiliate bundles everything into one plugin. There are a few use cases it doesn't cover that Siren handles through [distributors](/documentation/general/what-are-distributors). Solid Affiliate has no performance bonus system. There is no way to award a collaborator extra for hitting a sales target, generating the most revenue in a month, or outperforming other collaborators. Siren's distributors handle all of these cases. You configure which metrics to track, set a schedule (weekly, monthly, yearly), and choose how to distribute the reward: to the top performer, proportionally by score, or evenly among everyone who participated. Solid Affiliate also has no tiered rate system. Affiliate Groups let you set different static rates for different groups of affiliates, but there is no way to automatically increase a rate when a collaborator hits a threshold. In Siren, you can use a distributor to track cumulative performance and pay bonuses when collaborators reach milestones. Solid Affiliate's Sign-up Bonus awards a fixed amount when a new affiliate registers. Siren can handle this through a distributor, though depending on the setup a program may be more straightforward. If you're using Solid Affiliate's Subscription Renewal Referrals, Siren supports per-renewal commissions through the renewal conversion type in programs. For bonusing based on aggregate renewal counts over a period, a distributor handles that. ## Transaction filtering and commission calculations Solid Affiliate calculates commissions per line item by default, using the post-discount amount. There is no option to calculate on the pre-discount price. Shipping and tax each have toggles to include or exclude them. Fees are not addressed in the documentation. For product-level control, Solid Affiliate offers per-product rates, per-category rates, per-affiliate-per-product rates, and a "Disable Referrals" checkbox to exclude individual products. These work through a priority hierarchy where the most specific rate wins. Siren's [transaction filtering](/documentation/general/line-item-filters) gives you more control at both levels. For the commission base, each component (line items, discounts, fees, shipping, taxes) is an independent compiler that you enable or disable per program. Solid Affiliate hardcodes discounts as always subtracted and doesn't address fees. Siren lets you choose whether discounts factor in and includes a fees compiler for signup or subscription fees. For product-level control, Siren's line item filters take a composable approach instead of a rate hierarchy. You can filter by category, SKU, product type (products vs. subscriptions), or collaborator ownership, and filters combine with AND logic. This lets you express "only subscription products in these categories owned by the collaborator" as a single filter configuration. Solid Affiliate would require setting up per-affiliate-per-product rates on each qualifying product individually. Siren also supports SKU and product type filtering, which Solid Affiliate does not offer. ### Per-product, per-category, and per-affiliate-per-product rates Solid Affiliate lets you set commission rates on individual products, on product categories, and on specific affiliate-product pairings. If Product A should pay 30% for everyone except Affiliate X who gets 40%, you set both a product rate and an affiliate-product rate. In Siren, each distinct rate becomes its own program with line item filters targeting the relevant products or categories. A program paying 30% filtered to Product A's SKU covers the general case. A second program paying 40% filtered to the same SKU, with only Affiliate X enrolled, covers the override. Program groups can ensure only one fires per conversion if needed. The migration path is to audit your rate hierarchy and group by distinct rate. Solid Affiliate's 8-tier priority system (recurring rate, affiliate+product, affiliate, group, lifetime, product variation, product, category, default) flattens into a set of named programs in Siren. Each program's rate and filters are visible in one place, rather than requiring you to check multiple levels of overrides to understand what a given sale will pay. ## What migrates These Solid Affiliate records have direct equivalents in Siren and can be migrated. - Affiliates become collaborators - Referrals become conversions with corresponding obligations - Visits become opportunities - Payouts become payouts (within fulfillments) - Tracking codes become aliases (type: tracking) - Coupon links become aliases (type: coupon) - Affiliate groups inform which programs to create and which collaborators to enroll in each ## What doesn't migrate These Solid Affiliate features don't have equivalents in Siren. - Affiliate tags (no tagging system) - Landing page configurations (attribution works differently) - Store credit balances (payouts are monetary) - Linked customer relationships (lifetime commissions are roadmapped) - Custom affiliate fields (data can be stored as WordPress user meta, but there's no automatic migration path) ## Database table reference Solid Affiliate stores its data in custom database tables. Here's how they map to Siren's tables. | Solid Affiliate table | Siren equivalent | | --- | --- | | `wp_solid_affiliate_affiliates` | `wp_siren_collaborators` | | `wp_solid_affiliate_referrals` | `wp_siren_conversions` and `wp_siren_obligations` | | `wp_solid_affiliate_visits` | `wp_siren_opportunities` | | `wp_solid_affiliate_payouts` | `wp_siren_payouts` | | `wp_solid_affiliate_bulk_payouts` | `wp_siren_fulfillments` | | `wp_solid_affiliate_creatives` | No equivalent | | `wp_solid_affiliate_affiliate_groups` | No direct table equivalent (programs serve this role, stored in `wp_siren_programs`) | | `wp_solid_affiliate_coupons` | `wp_siren_collaborator_aliases` (type: coupon) | | `wp_solid_affiliate_affiliate_customers` | No equivalent (lifetime commissions roadmapped) | Table names assume the default `wp_` prefix. Your site may use a different prefix. ## Next steps Once you've reviewed the mappings above, the actual migration process involves creating programs that match your current affiliate group structure, importing collaborator records, and bringing over historical conversion and obligation data. The [migration overview](/documentation/migration/overview) covers the general process and what to expect during the transition. ## Migration Overview Source: https://www.sirenaffiliates.com/documentation/migration/overview How Siren's architecture differs from other affiliate plugins, what maps to what, and what to expect when switching. # Migrating to Siren If you're coming from another WordPress affiliate plugin, the core workflow will feel familiar. You have partners, they promote your products, and you pay them when sales happen. But Siren's architecture differs from other tools in ways that matter, and understanding those differences before you start will make the transition smoother. This page covers the structural differences that apply regardless of which plugin you're migrating from. For field-by-field mapping tables and platform-specific details, see the individual guides: - [Migrating from AffiliateWP](/documentation/migration/affiliatewp) - [Migrating from SliceWP](/documentation/migration/slicewp) - [Migrating from Easy Affiliate](/documentation/migration/easy-affiliate) - [Migrating from Solid Affiliate](/documentation/migration/solid-affiliate) ## Use Beacon for migration assistance There isn't an automated importer that moves your old plugin's data into Siren today. Migration is a manual concept-mapping exercise, and the fastest way through it is to work with [Beacon](/documentation/getting-started/what-is-beacon), Siren's free AI assistant. Beacon knows Siren's architecture inside and out, so you can describe your existing setup in plain English (current programs, commission rates, payout structures, integrations) and it'll help you translate the pieces into a Siren recipe you can install. If you're not sure whether a specific feature from your old plugin has an equivalent in Siren, ask Beacon before you start digging through the mapping tables below. Start by describing your current program to Beacon and asking for a recipe that matches. Use the platform-specific guides below as a reference for the terminology and edge cases Beacon should know about. ## How Siren thinks about incentive programs differently The biggest difference between Siren and other affiliate plugins is where the rules live. In most affiliate plugins, commission rates are attached to affiliates. You set a global rate (say, 20%), then override it per affiliate when someone negotiates a better deal. The affiliate is the center of the system, and the commission structure is a property of the affiliate. Siren flips this. Commission rules live in [programs](/documentation/general/what-are-programs), and collaborators are enrolled in those programs. A program defines what triggers a reward, how the reward is calculated, and who is eligible. If you want different rates for different situations, you create different programs instead of per-affiliate overrides. This means you can run an affiliate program that pays 20% on sales alongside a content creator royalty program that pays 50% on courses, alongside a performance bonus program that distributes a pool of rewards monthly. Each program has its own rules. The same collaborator can be enrolled in all three and earn from each one independently. In other plugins, achieving this kind of setup requires creative workarounds or isn't possible at all. In Siren, it's the default way the system works. ## One sale, multiple payouts In AffiliateWP, SliceWP, and most other plugins, a sale creates one referral (or commission) record. One sale, one payout, one affiliate gets credit. Siren separates the "what happened" from the "what's owed." When a sale occurs, Siren creates a [conversion](/documentation/general/what-is-a-conversion), a record that says "this sale was attributed to this collaborator through this program." The system then calculates what's owed and creates an [obligation](/documentation/general/what-are-obligations), a record that says "you owe this collaborator this amount." Because conversions and obligations are separate, one sale can create multiple obligations. If a customer clicks an affiliate link from Steve and then buys a course created by Brad, Steve might earn a 25% affiliate commission and Brad might earn a 50% royalty. Two obligations from the same transaction. The programs don't interfere with each other because they're tracking different contributions. If you only ever need one affiliate earning one commission per sale, this separation is invisible. It works the same way. But when your incentive structures grow beyond the basics, the separation is what makes it possible without workarounds. This also means obligations can exist without conversions. If you need to pay a collaborator for something that didn't come through the normal attribution pipeline, like a negotiated bonus or a manual adjustment, you can create an obligation directly. ## Programs replace global settings and per-affiliate overrides Most affiliate plugins have a settings page where you configure one global commission rate, then provide per-affiliate or per-product overrides when the global rate doesn't fit. The more exceptions you add, the harder the configuration is to reason about. Siren doesn't have global commission settings. Instead, each program defines its own rates, triggers, and eligibility rules. If you need different rates for different situations, you create a program for each situation rather than layering overrides on top of a global default. [Program groups](/documentation/general/what-are-program-groups) handle the case where multiple programs could apply to the same sale. A program group bundles related programs and ensures only one fires per conversion, using rules like "newest engagement wins" or "oldest engagement wins" to break ties. If you're a Pro user coming from a tiered or override-heavy competitor setup, a [collaborator group](/documentation/general/what-are-collaborator-groups) with an Upline [Cascade](/documentation/general/what-is-a-cascade) is often a closer fit than a program group. You arrange your tiers as positions in a linear chain and credit each layer with per-layer points, so promotions become position moves in the chain instead of program membership changes. See [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade) for the conversion path, and [Upgrading to Plus and Pro](/documentation/migration/upgrading-to-plus-and-pro) for what changes when you move up a tier to get groups and cascades. ## Distributors handle bonuses and scheduled rewards Programs pay out per transaction. When a sale happens, the program evaluates the conversion and creates an obligation. But some incentive structures don't work that way. Performance bonuses, profit shares, and milestone rewards are based on what someone did over a period of time, not on a single sale. That's what [distributors](/documentation/general/what-are-distributors) are for. A distributor tracks collaborator performance over a period (weekly, monthly, or yearly), accumulates metric scores based on tracked events, and then distributes rewards when the period ends. The reward can go to the top performer, be split proportionally by score, or be divided evenly among everyone who participated. Most affiliate plugins don't have anything like this. Some offer add-ons for performance bonuses or tiered rates, but those tend to be limited to a few fixed metrics and simple threshold logic. Siren's distributors can track any combination of events (sales, course completions, blog visits, coupon uses, and more) and use configurable point values to weigh different contributions. The individual migration guides cover which competitor add-ons map to distributors and how. If you're coming from a plugin where you've been working around the lack of scheduled rewards, distributors are likely the feature that will change the most about how you think about your incentive programs. ## Tracking is more granular Other plugins track referral link clicks as "visits," a single record that ties a click to an affiliate. Siren breaks this into two concepts. An [opportunity](/documentation/general/what-is-an-opportunity) represents a trackable visitor. When someone clicks a referral link, Siren creates an opportunity that says "this person arrived through this referral mechanism." An [engagement](/documentation/general/what-is-an-engagement) represents a credit claim against a specific program. One opportunity can create multiple engagements if the collaborator is enrolled in multiple programs. Each engagement tracks the collaborator's claim separately, with its own score that program groups can use to determine which program wins. For migration purposes, a "visit" from another plugin maps most closely to an opportunity. Engagements don't have a direct equivalent in other systems because other systems don't have multi-program attribution. ## Commission calculations are modular Most affiliate plugins make fixed decisions about how commissions are calculated. Discounts are always subtracted before the commission is computed. Shipping and tax might have a toggle to include or exclude them. Fees are silently included or ignored. You get a few checkboxes and the rest is hardcoded. Siren uses a modular system called [transaction compilers](/documentation/general/line-item-filters). Each component of a transaction (line items, discounts, fees, shipping, taxes) is an independent module that you enable or disable per program or per distributor. If you want commissions calculated on the pre-discount amount, don't enable the discounts compiler. If you want subscription signup fees included in the calculation, enable the fees compiler. Each combination produces a different commission base, and you choose what makes sense for each program. On top of that, the line items compiler has composable filters that narrow which products in a transaction are eligible. You can filter by product category, SKU, product type (products vs. subscriptions), or collaborator ownership. Multiple filters combine with AND logic, so a single transaction with five items might only pay commission on the two that pass all filters. No competitor offers this level of modularity. The best any competitor does is two toggles (shipping and tax). None of them let you choose whether discounts are factored in, none have a fee toggle, and none offer SKU-based or product-type-based filtering. Most affiliate plugins handle per-product and per-category commission rates by putting rate fields directly on product edit screens or in category settings. If Product A should pay 30% and Product B should pay 15%, you set those rates on each product. Siren doesn't have per-product rate fields. Instead, you create a program with the rate you want and use line item filters to control which products it applies to. A 30% program filtered to Product A and a 15% program filtered to Product B achieves the same result. This is a different workflow, and it requires more upfront setup when you have many distinct rates. The advantage is that all commission logic lives in named programs rather than being scattered across individual product settings. It also means filters compose with each other. A program that pays 25% only on subscription products in the "courses" category owned by the collaborator is a single program configuration. In other plugins, you cannot combine product-level rates with category rates with affiliate-specific rates in a single coherent rule. The individual migration guides cover the specific per-product and per-category capabilities of each plugin and how to restructure them as Siren programs. ## Collaborators are WordPress users In Siren, every collaborator is a real WordPress user account. AffiliateWP does this too, but Siren leans into it more heavily. The collaborator portal, role-based permissions, and dashboard access all build on WordPress's native user system. The term "collaborator" instead of "affiliate" reflects a broader intent. An affiliate is someone who promotes products through referral links. A collaborator might do that, but they might also be a course creator earning royalties, a co-founder tracking profit shares, or a salesperson earning bonuses. The label is deliberately wider because Siren's program system supports all of these roles. ## What Siren doesn't have (yet) A few features common in other affiliate plugins aren't in Siren today. Most affiliate plugins include a dedicated section for managing creatives like banners, text links, and promotional materials. Siren doesn't, because WordPress's block editor makes it straightforward to create pages with promotional materials. If you need a structured creative library, a custom post type handles it. In practice, customers haven't needed a built-in system for this. Lifetime commissions, where a customer is permanently linked to an affiliate so the affiliate earns on all future purchases, are on the roadmap but not implemented yet. Campaign tracking lets affiliates tag their referral links with a parameter like `&campaign=email` to see which channel drove traffic. This could be added if customers need it, but nobody has asked for it so far. Some plugins support tiered or override commission structures, where a manager or partner earns on the sales of the team or partners beneath them. On the Pro tier, Siren handles this with [collaborator groups](/documentation/general/what-are-collaborator-groups) and [cascades](/documentation/general/what-is-a-cascade). You arrange collaborators into a hierarchical group (a linear chain or a parent-child tree), bind the group to a program or distributor, and use a Downline Cascade calculation strategy to credit the collaborators below them, layer by layer, when they trigger an event. A Downline Cascade walks toward the leaves of the chain, so a collaborator earns from the people below them. An Upline Cascade walks the other direction, crediting the people above. The triggering collaborator is never credited by the cascade itself. For setups with tiered or override-based commissions, such as a sales hierarchy or a channel partner program, see [Migrating a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade). ## What to expect during migration The typical migration involves moving three things: affiliate accounts, historical commission data, and tracking codes. Affiliate accounts map to collaborators. The user accounts themselves may already exist in WordPress. The migration creates collaborator records linked to those users, enrolls them in the appropriate programs, and assigns tracking aliases that match their existing referral codes so published links continue to work. Historical commissions can be imported as completed conversions and fulfilled obligations. The data won't participate in Siren's attribution pipeline (it's historical, not active), but it preserves the record so collaborators can see their earning history. Tracking codes become [aliases](/documentation/resource-reference/aliases) in Siren. If an affiliate's referral link used `?ref=123` or `?ref=janedoe`, that code is stored as an alias so the URL continues to resolve. Siren's alias system preserves history, so even if the code is later changed, the old one keeps working. Active browser cookies from the old plugin won't transfer. If a customer clicked an affiliate link before the migration but hasn't purchased yet, that attribution will be lost unless you handle it manually. This is a narrow window for most sites, but worth noting if you have long cookie durations. See the platform-specific guides for detailed field mappings and step-by-step considerations. ## Newest Engagement Wins Source: https://www.sirenaffiliates.com/documentation/program-group-structures/newest-engagement-wins A program group structure where the program containing the most recent engagement with the customer is the one that runs on conversion. Newest Engagement Wins is a [program group](/documentation/general/what-are-program-groups) structure that decides which [program](/documentation/general/what-are-programs) in the group runs when a customer converts. Only one program in a group runs per conversion, and this structure picks the program whose collaborators had the most recent [engagement](/documentation/general/what-is-an-engagement) with the customer. This operates at the group selection level, not inside an individual program. Once Siren picks the winning program, that program's own structure takes over to determine which collaborators get paid and how much. ## How it works Suppose a group contains two programs: a standard affiliate program and a super-affiliate coupon program. A customer clicks a standard affiliate's link in January, browses the site, and then in March uses a super-affiliate's coupon at checkout. When the conversion fires, Siren checks the newest engagement across both programs. The coupon use in March is more recent than the click in January, so the super-affiliate coupon program runs and the standard affiliate program sits out for that conversion. If the customer had instead checked out without the coupon, the standard affiliate program would have run because its January click would be the only engagement in the group. ## Where this works This is the right structure when the most recent touchpoint is the one you want to credit. It's also the standard choice for groups where a closer (coupon, discount code, last-click ad) should override a broader awareness program if both fired for the same customer. The [basic affiliate program](/recipes/basic-affiliate-program) and [tiered affiliate program](/recipes/tiered-affiliate-program) recipes both assume this selection model when they're placed in a group alongside competing programs. It gives last-click attribution at the group level while letting each individual program handle payout internally however it wants. If you are using a program group to build affiliate tiers, by promoting collaborators between a standard program and a VIP program, there is a more direct option on Siren Pro. Bind a single program to a [linear-chain collaborator group](/documentation/general/what-are-collaborator-groups) and use the [Upline Cascade calculation strategy](/documentation/calculation-strategies/upline-cascade). Tiers then come from a collaborator's position in the chain, so promoting someone is a position move instead of moving them between programs, which removes the membership churn that program-group tiers create on every promotion. The [migrate a tiered program group to a cascade](/documentation/migration/migrate-tiered-program-group-to-cascade) guide walks an existing two-program tier setup through the conversion. ## When to avoid this If you want to credit the program whose collaborators brought the customer in first (rewarding lead generation over closing), use [Oldest Engagement Wins](/documentation/program-group-structures/oldest-engagement-wins) instead. If you want multiple programs in the group to run on the same conversion, don't group them at all. Program groups exist specifically to force mutual exclusivity, so programs that should all pay out independently belong as separate ungrouped programs. ## Notes Source: https://www.sirenaffiliates.com/documentation/resource-reference/notes Activity-feed entries written by lifecycle events — data model, source linking, blueprint keys, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; # Notes A note is a single entry in Siren's activity feed. Lifecycle events across the system (a conversion being approved, an obligation being issued, a payout being marked paid, and so on) each write a note and link it to every related record. Reading the notes for a record gives you a chronological history of everything Siren has done to that record. In the admin UI the collection of notes for a given record is surfaced as the [activity feed](/documentation/general/activity-feeds). Notes are written by the system. The REST and PHP APIs expose them for reading, rendering, and searching; there is no public endpoint for creating or editing notes by hand. If you need a note to exist, fire the event that the blueprint listens for. ## The note object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Content | `content` | `getContent()` | string | The blueprint key (for example, `obligation_issued`). The displayable text is resolved from this key at read time. | | Creation source | `creationSource` | `getCreationSource()` | string | The entity type that caused the note to be written (for example, `conversion`). | | Creation source ID | `creationSourceId` | `getCreationSourceId()` | integer | The ID of the entity that caused the note to be written. | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the note was recorded. | Notes intentionally do not store rendered text. Storing the blueprint key and resolving at read time means that when a source entity changes (for example, a collaborator's display name is updated), every historical note that mentions that collaborator reflects the change automatically. ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically at request time by the field resolver system and must be explicitly requested. | Field | Type | Description | |---|---|---| | `renderedContent` | string | The note's blueprint rendered with source data substituted in (for example, `Obligation #42 issued for Jane Smith`). This is what the activity feed UI displays. | | `sourceData` | object | The raw data array the blueprint was resolved against. Use this when you want to format notes yourself instead of using `renderedContent`. | | `sources` | array | The list of source entities this note is linked to, as `{sourceType, sourceId}` pairs. A single note typically links to several sources (the conversion, the obligation, and the collaborator all share the same `obligation_issued` note). | ## Blueprint keys Every note stores a blueprint key in its `content` field. The blueprint registry maps each key to a template string with `{placeholder}` tokens, which the resolver system fills in at read time. Siren ships with 22 core blueprints covering the full lifecycle. | Key | Written when | |---|---| | `obligation_issued` | An obligation is created for a conversion. | | `obligation_completed` | An obligation is rolled into a payout. | | `conversion_awarded` | A conversion is awarded to a collaborator. | | `conversion_approved` | A pending conversion is approved. | | `conversion_rejected` | A conversion is rejected manually or by a refund. | | `conversion_renewed` | A renewal transaction produces a renewed conversion. | | `renewed_conversions_awarded` | Renewed conversions are awarded under a program. | | `payout_created` | A payout record is created for a collaborator. | | `payout_paid` | A payout is marked as paid. | | `fulfillment_created` | A fulfillment batch is created. | | `fulfillment_status_changed` | A fulfillment's status changes. | | `collaborator_account_ready` | A collaborator account is activated. | | `collaborator_submission_complete` | A collaborator application is submitted. | | `opportunity_invalidated` | An opportunity is invalidated. | | `manual_attribution_requested` | A transaction is manually attributed to a collaborator. | | `program_group_conversion_triggered` | A program wins a program-group competition for a transaction. | | `engagement_awarded` | An engagement is awarded to a collaborator. | | `refund_triggered` | A refund fires for a transaction. | | `coupon_applied` | A bound coupon is applied. | | `allocation_completed` | A distribution allocation is completed. | | `distribution_completed` | A distributor completes a distribution cycle. | | `org_created` | A new organization is created (multi-site installs). | Extensions can register additional blueprints by listening for the `NoteBlueprintRegistryInitiated` event and calling `$event->addNote($key, $resolver)`. ## Source linking A note is rarely useful on its own. What makes the activity feed work is that each note is linked to every record the underlying event touched. An `obligation_issued` note is linked to the obligation, the conversion that caused it, and the collaborator who is owed the money. Open any of those three detail screens and the same note appears in the feed. Source links live in the `NoteSources` junction table. Each row is a `{noteId, sourceType, sourceId}` tuple. The supported source types are the major domain records: `collaborator`, `conversion`, `obligation`, `fulfillment`, `payout`, `engagement`, `opportunity`, `transaction`, `distribution`, `program`, and `distributor`. Not every note links to every type. A `coupon_applied` note, for example, links to the opportunity and the collaborator, but not to a conversion, because no conversion exists yet when the coupon is applied. The REST API exposes notes by source, not by raw note ID. You ask "what notes exist for collaborator 42?" and the response is the list of every note linked to that collaborator in the order the events happened. ## Accessing note data ```bash # Fetch the activity feed for an obligation curl -X GET "https://your-site.com/wp-json/siren/v1/notes?sourceType=obligation&sourceId=42&fields=id,renderedContent,creationSource,dateCreated" \ -H "Authorization: Bearer YOUR_TOKEN" # Same feed, but return raw source data so you can format it yourself curl -X GET "https://your-site.com/wp-json/siren/v1/notes?sourceType=obligation&sourceId=42&fields=id,content,sourceData,sources,dateCreated" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Notes\Core\Datastores\Note\Interfaces\NoteDatastore; class ActivityFeedReader { protected NoteDatastore $notes; public function __construct(NoteDatastore $notes) { $this->notes = $notes; } public function getFeedForObligation(int $obligationId): array { return $this->notes->getNotesForSource('obligation', $obligationId); } } ``` ```php use Siren\Notes\Core\Facades\Notes; $feed = Notes::getNotesForSource('obligation', 42); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Notes are written by `CreateNoteFor*` listeners that respond to lifecycle events. Creating notes by calling the datastore directly bypasses the source-linking logic that makes the activity feed work, so the resulting entries won't appear in any record's feed. If you want a note to exist, fire (or add a listener to) the event the blueprint is designed for. ## REST endpoints | Method | Path | Description | |--------|------|-------------| | GET | `/notes` | [List notes for a source](/documentation/resource-reference/notes/list) | | GET | `/notes/:id/position` | [Get a note's position in its feed](/documentation/resource-reference/notes/position) | See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## Relationships Every note is linked to one or more source records via the `NoteSources` junction table. Those sources span the full domain model: - [Collaborators](/documentation/resource-reference/collaborators) — a note is linked to every collaborator mentioned in the underlying event. - [Conversions](/documentation/resource-reference/conversions), [Obligations](/documentation/resource-reference/obligations), [Fulfillments](/documentation/resource-reference/fulfillments), [Payouts](/documentation/resource-reference/payouts) — the payment pipeline records. - [Engagements](/documentation/resource-reference/engagements), [Opportunities](/documentation/resource-reference/opportunities), [Transactions](/documentation/resource-reference/transactions) — the attribution records. - [Distributions](/documentation/resource-reference/distributions), [Programs](/documentation/resource-reference/programs), [Distributors](/documentation/resource-reference/distributors) — the configuration records when the event is scoped to them. Notes are written as a side effect of the normal event pipeline. If you're adding a new event to Siren, registering a blueprint for it makes it show up in the activity feed automatically. See the [events reference](/documentation/developer-reference/events-introduction) for the full event catalog. ## ObligationCompleted Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/obligation-completed Fires when an obligation has been fulfilled through a payout. # ObligationCompleted When an obligation has been settled through a payout, Siren fires `ObligationCompleted`. At this point the obligation's status has been updated to fulfilled and the payout ID has been written to the obligation record. The debt the business owed the collaborator is considered resolved. The event is identified as `obligation_completed` and lives in `Siren\Obligations\Core\Events\ObligationCompleted`. ## What does this event carry? The event provides the `Obligation` model and the `payoutId` of the payout that settled the debt. ```php use Siren\Obligations\Core\Events\ObligationCompleted; public function handle(Event $event): void { $obligation = $event->getObligation(); $payoutId = $event->getPayoutId(); } ``` ## When would you use it? This event signals that a specific payment commitment has been resolved. Listeners might use it to update dashboards, send confirmation messages to collaborators, or reconcile records in external financial systems. The obligation carries the collaborator ID and value, so listeners can determine who was paid and how much without needing to look up the payout separately. For the event that fires when the obligation is first created, see [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued). For the payout-level events that track the disbursement itself, see [PayoutCreated](/documentation/developer-reference/events-payments/payout-created) and [PayoutPaid](/documentation/developer-reference/events-payments/payout-paid). See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## ObligationIssued Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/obligation-issued Fires when an obligation is created for a conversion, representing a debt the business owes a collaborator. # ObligationIssued An obligation is a debt the business owes a collaborator as a result of a conversion. When that record is created, Siren fires `ObligationIssued` to signal that a new payment commitment exists. This is the bridge between the conversion pipeline and the payout pipeline. The event is identified as `obligation_issued` and lives in `Siren\Obligations\Core\Events\ObligationIssued`. ## What does this event carry? The event provides the `Obligation` model and an optional `conversionId`. The conversion ID links the obligation back to the conversion that caused it. It is optional because some obligation creation paths, like manual adjustments, may not originate from a conversion at all. ```php use Siren\Obligations\Core\Events\ObligationIssued; public function handle(Event $event): void { $obligation = $event->getObligation(); $conversionId = $event->getConversionId(); $collaboratorId = $obligation->getCollaboratorId(); $value = $obligation->getValue(); } ``` ## What happens when it fires? The primary downstream listener is `BindObligationToConversion`, which writes the obligation's ID back to the conversion record. This closes the two-way link between the conversion and the obligation, so either record can locate the other. Other listeners might use this event to update running totals, notify collaborators that a commission has been earned, or sync obligation data to external accounting systems. Once the obligation is eventually settled through a payout, the system fires [ObligationCompleted](/documentation/developer-reference/events-payments/obligation-completed). See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## Obligations Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations Payment obligations owed to collaborators — data model, status lifecycle, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; import FlowchartSteps from "@/components/blocks/FlowchartSteps.astro"; # Obligations An obligation represents a debt the business owes a [collaborator](/documentation/resource-reference/collaborators) as a result of an approved [conversion](/documentation/resource-reference/conversions). Obligations sit downstream of conversions and upstream of [fulfillments](/documentation/resource-reference/fulfillments). They are the "IOU" in Siren's [attribution pipeline](/documentation/resource-reference/pipeline-overview). When a conversion is approved, the incentive system evaluates the program's rules and creates an obligation for the calculated reward amount. Once obligations are gathered into a fulfillment batch and paid out, their status transitions to `fulfilled`. ## The obligation object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Collaborator | `collaboratorId` | `getCollaboratorId()` | integer | The collaborator this obligation is owed to | | Status | `status` | `getStatus()` | string | Current status (see lifecycle below) | | Award type | `awardType` | `getAwardType()` | string | How the reward was calculated (e.g., `commission`, `flat`) | | Value | `value` | `getValue()` | integer | Reward amount in the smallest currency unit (e.g., cents) | | Payout | `payoutId` | `getPayoutId()` | integer or null | The payout this obligation was fulfilled through, if any | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the record was created | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the record was last updated | The `value` field stores amounts as integers in the smallest currency unit. A $15.00 commission is stored as `1500`. This avoids floating-point precision issues in financial calculations. The `payoutId` is null until the obligation is included in a payout batch. Once a payout is processed and the obligation is marked `fulfilled`, the `payoutId` links it to the payout record that settled the debt. ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested. **List endpoint** (`GET /obligations`): `collaboratorName`, `collaboratorEmail` **Detail endpoint** (`GET /obligations/{id}`): `collaboratorName`, `collaboratorEmail`, `conversionId`, `conversionType`, `conversionDate`, `conversionAmount`, `programId`, `programName` ## Status lifecycle | Status | Description | |---|---| | `draft` | Initial state when a conversion is recorded but not yet approved. The obligation exists but is not actionable. | | `pending` | The conversion has been approved and the obligation is awaiting fulfillment. | | `fulfilled` | Included in a fulfillment batch. The payoutId field links it to the specific payout record. | | `rejected` | The linked conversion was rejected (e.g., due to a refund). The admin can cancel or leave pending depending on refund policy. | | `cancelled` | Soft deleted. A second DELETE request permanently removes the record. | See [How Refunds Work](/documentation/general/how-refunds-work) for a complete walkthrough of how refunds cascade through conversions and obligations. ## Accessing obligation data ```bash # List pending obligations for a collaborator curl -X GET "https://your-site.com/wp-json/siren/v1/obligations?collaboratorId=42&status=pending&fields=id,value,status,awardType" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single obligation with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/obligations/15?fields=id,value,status,collaboratorName,programName" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Obligations\Core\Datastores\Obligation\Interfaces\ObligationDatastore; class PayoutReport { protected ObligationDatastore $obligations; public function __construct(ObligationDatastore $obligations) { $this->obligations = $obligations; } public function getPendingObligations(int $collaboratorId): array { return $this->obligations->andWhere([ ['column' => 'collaboratorId', 'operator' => '=', 'value' => $collaboratorId], ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); } } ``` ```php use Siren\Obligations\Core\Facades\Obligations; $pending = Obligations::andWhere([ ['column' => 'collaboratorId', 'operator' => '=', 'value' => $collaboratorId], ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); $obligation = Obligations::getById(42); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Creating obligations manually via PHP bypasses the incentive calculation. The program's reward rules won't be applied, the conversion won't be linked, and the `ObligationIssued` event won't fire. Downstream listeners that bind obligations to conversions and trigger fulfillment workflows depend on that event. If you need to issue an obligation, let the pipeline handle it by approving the conversion. ## PHP domain methods ### Looking up an obligation by conversion `getByConversionId` retrieves the obligation that was created for a specific conversion. This is useful when you have a conversion record and need to check whether an obligation was issued and what its current state is. This method is only available through dependency injection. ```php use Siren\Obligations\Core\Datastores\Obligation\Interfaces\ObligationDatastore; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; class ConversionInspector { protected ObligationDatastore $obligations; public function __construct(ObligationDatastore $obligations) { $this->obligations = $obligations; } public function getObligationForConversion(int $conversionId): ?Obligation { try { return $this->obligations->getByConversionId($conversionId); } catch (RecordNotFoundException $e) { return null; } } } ``` Not every conversion results in an obligation. The incentive system may determine the conversion doesn't qualify, or the obligation may not have been created yet if the conversion is still pending. If you already have the conversion's `obligationId`, you can skip this lookup and use `getById` directly: ```php $obligationId = $conversion->getObligationId(); if ($obligationId !== null) { $obligation = $this->obligations->getById($obligationId); } ``` ### Querying by payout To find all obligations that were settled in a specific payout batch: ```php $settled = $this->obligations->andWhere([ ['column' => 'payoutId', 'operator' => '=', 'value' => $payoutId] ]); ``` ### Summing owed amounts There is no built-in sum method, but you can combine a filtered query with array reduction: ```php $pending = $this->obligations->andWhere([ ['column' => 'collaboratorId', 'operator' => '=', 'value' => $collaboratorId], ['column' => 'status', 'operator' => '=', 'value' => 'pending'] ]); $totalOwed = array_reduce($pending, fn(int $sum, Obligation $o) => $sum + $o->getValue(), 0); ``` ## Access control (REST) The list endpoint uses `AutoFilterByOwnerMiddleware` to enforce access control. Admin users see all obligations across all collaborators, while collaborator-role users are automatically filtered to see only obligations linked to their own `collaboratorId`. The middleware maps the authenticated user to their collaborator record and injects the filter before the query executes, which is how the collaborator portal shows "my earnings" without exposing other collaborators' data. ## Relationships Every obligation belongs to exactly one [collaborator](/documentation/resource-reference/collaborators) via `collaboratorId`. Upstream, obligations are created as a downstream effect of an approved [conversion](/documentation/resource-reference/conversions). The link between the two is maintained via the conversion's `obligationId` field. When an obligation is fulfilled, it is linked to a [payout](/documentation/resource-reference/payouts) record via `payoutId`. The payout tracks the actual disbursement to the collaborator. Payouts are grouped into [fulfillment](/documentation/resource-reference/fulfillments) batches, so the obligation connects to the fulfillment indirectly through its payout. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## Oldest Engagement Wins Source: https://www.sirenaffiliates.com/documentation/program-group-structures/oldest-engagement-wins A program group structure where the program containing the first engagement with the customer is the one that runs on conversion. Oldest Engagement Wins is a [program group](/documentation/general/what-are-program-groups) structure that decides which [program](/documentation/general/what-are-programs) in the group runs when a customer converts. Only one program in a group runs per conversion, and this structure picks the program whose collaborators had the earliest [engagement](/documentation/general/what-is-an-engagement) with the customer. This operates at the group selection level, not inside an individual program. Once Siren picks the winning program, that program's own structure takes over to determine which collaborators get paid and how much. This is a program group structure, which is a different setting from a [collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure). A program group structure picks one winning program per conversion. A collaborator group structure (flat, linear-chain, or parent-child) shapes how a cascade walks a roster to credit peers across layers. See [what are collaborator groups](/documentation/general/what-are-collaborator-groups) if that is what you need. ## How it works Suppose a group contains a lead-generation affiliate program and a sales-closer program. A customer clicks a lead-gen affiliate's link in January and lands on a blog post. Months later in April, a sales rep in the closer program walks the same customer through a demo and the customer converts. When the [conversion](/documentation/general/what-is-a-conversion) fires, Siren looks at the oldest engagement across both programs. The January click is older than the April demo, so the lead-gen program runs and the closer program sits out for this sale. If the customer had converted with no prior engagement, only the closer program's engagement would exist and that program would run instead. ## Where this works This structure is the right choice when you want to credit the program whose collaborators originated the lead, not the one that closed the sale. It's useful for paying affiliates who bring in top-of-funnel traffic even when a separate sales or closer program eventually seals the deal. The [first-touch referral program](/recipes/first-touch-referral-program) recipe is built around this selection model. Pair it with a closer program in the same group and the referral program wins any conversion where the referral came first, while the closer program only runs on customers with no prior referral. ## When to avoid this If the most recent touchpoint is the one driving the purchase decision (coupons, last-click ads, closers), use [Newest Engagement Wins](/documentation/program-group-structures/newest-engagement-wins) instead. This structure also isn't a fit if you want both programs in the group to pay out on the same sale. Program groups force mutual exclusivity by design, so if lead-gen and closers should both be compensated for every conversion, run them as separate ungrouped programs. ## Operating a cascade program Source: https://www.sirenaffiliates.com/documentation/getting-started/operating-a-cascade-program Manage a live cascade group over time. Add, remove, and reorder members from the admin UI, understand the membership endpoints, and see how membership changes affect an in-flight cascade. import StepList from "@/components/content/StepList.astro"; import Screenshot from "@/components/content/Screenshot.astro"; Setting up a cascade is a one-time job. Running it is ongoing. People join the team, leave the team, and move up or down the chain. This page covers the day-to-day work of keeping a live [cascade](/documentation/general/what-is-a-cascade) group accurate, and what happens to an in-flight cascade when its membership changes. If you haven't built a cascade yet, start with [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group) and [Configure cascade payouts](/documentation/getting-started/configure-cascade-payouts). This page assumes a group already exists and is bound to a program or distributor. ## Editing membership from the admin UI Open the group from the Collaborator Groups list under the Siren menu. The edit screen shows the members table. Every membership change happens here, and nothing is persisted until you click Save. ### Add members Click Add Members to open the collaborator picker modal. Search by name or email, check the collaborators you want, and stage them. Staged rows appear in the members table with a pending indicator. Click Save to commit them. Adding a member who is already in the group is a no-op. The add path skips any collaborator that already has a membership row, so you can re-run an add without creating duplicates. ### Remove members Each row has a remove control. Removing a row stages the removal. As with adds, nothing leaves the group until you Save. ### Reorder members Reordering only applies to [linear chain](/documentation/collaborator-group-structures/linear-chain) and [parent-child](/documentation/collaborator-group-structures/parent-child) groups. Flat groups have no order, so there is nothing to arrange. In a linear chain group, drag a row by its handle to move it up or down. Position 1 sits at the top and is the most upline. Siren recomputes the position values on save based on the visual order, so you never type a number. In a parent-child group, drag to reparent or use the Parent dropdown to pick a parent directly. Siren prevents cycles, so you can't make a collaborator its own descendant's parent. Because reordering changes per-member metadata (the `position` for a chain, the `parentCollaboratorId` for a tree), reordering a live group changes how the cascade walks it. See [How membership changes hit an in-flight cascade](#how-membership-changes-hit-an-in-flight-cascade) below. {/* Screenshot needed: members table on a linearChain group edit screen, showing drag handles and one staged/pending row before save. */} ## The membership endpoints The admin UI drives three REST endpoints under the group's `/members` path. You'll see these if you script membership changes or read the network traffic while editing a group. All three require the Update capability on `CollaboratorGroup`, which only an administrator or Platform Manager holds. | Endpoint | Method | What it does | |---|---|---| | `AddCollaboratorGroupMembers` | `POST /collaborator-groups/{id}/members` | Bulk-adds members. Collaborators that already have a row are skipped. Returns the newly-added rows. | | `RemoveCollaboratorGroupMember` | `DELETE /collaborator-groups/{id}/members/{collaboratorId}` | Removes a single collaborator. Returns 204 on success, 404 when the membership row does not exist. | | `SetCollaboratorGroupMembers` | `PUT /collaborator-groups/{id}/members` | Full-replace of the member set. Reconciles the submitted list against what's there: adds new collaborators, removes ones no longer listed, and updates metadata on the rest. | `AddCollaboratorGroupMembers` and `SetCollaboratorGroupMembers` take a `members` array of `{ "collaboratorId": int, "metadata": object }` entries, where `metadata` is where chain `position` or tree `parentCollaboratorId` lives. `SetCollaboratorGroupMembers` is the one the UI uses for a full save, because it reconciles the whole roster in a single call instead of issuing separate add and remove requests. ## What CollaboratorGroupMemberMetadataChanged means `SetCollaboratorGroupMembers` reconciles the roster member by member. For each collaborator it does one of three things, and each path broadcasts a different event: - A collaborator in the submitted list who has no current row is created, which broadcasts `CollaboratorAddedToCollaboratorGroup`. - A collaborator with a current row whose submitted metadata differs from the stored metadata is updated, which broadcasts `CollaboratorGroupMemberMetadataChanged`. - A collaborator with a current row who is absent from the submitted list is deleted, which broadcasts `CollaboratorRemovedFromCollaboratorGroup`. `CollaboratorGroupMemberMetadataChanged` (event id `collaborator_group_member_metadata_changed`) fires only when an existing member's metadata actually changes. It carries the `groupId`, the `collaboratorId`, the `previousMetadata`, and the `newMetadata`. If the submitted metadata matches what's already stored, no update runs and no event fires. This is the event that tells you a member was reordered or reparented rather than added or removed. In practice that means a chain member's `position` changed, or a tree member's parent changed, while the member stayed in the group. ## How membership changes hit an in-flight cascade A cascade is computed at trigger time, not stored as a fixed payout plan. It walks the bound group's current structure each time an engagement or metric fires. So membership changes take effect on the next trigger, not retroactively. Credits already produced by past triggers are independent. They are recorded as engagements and obligations that don't recompute when you change the group. Editing the roster shapes future cascades, not ones that already fired. Three changes are worth thinking through before you make them on a live group. ### Reordering changes who earns which layer Layer numbers are derived from the walker's position in the chain or tree, not from a stored value on the member. When you reorder a chain or reparent a tree node, the next trigger walks the new shape. A collaborator you moved up the chain starts earning a nearer (higher-value) layer, and the one you moved down earns a further one. Nothing about past credits changes. The new order governs the next walk. ### Removing a member shortens the walk for everyone below them Removing a member from a chain or tree closes the gap. The members that were below the removed one each move up by a layer on the next trigger, because the walk no longer steps through the missing member. If you remove someone mid-chain, the layer they used to occupy is gone, and the cascade continues straight through to the next member. ### Suspending versus deleting a chain member There are two distinct cases, and they behave differently from each other and from a removal. A collaborator who is still a group member but is not active (suspended, pending, or otherwise not `active`) is skipped by the cascade with no credit, and their layer simply goes unpaid for that trigger. The cascade keeps walking, but it does not renumber and does not move anyone else into the empty layer. Every other member keeps their own layer and per-layer rate, so the person below a suspended member does not move up and does not collect the suspended layer's credit. In a [parent-child](/documentation/collaborator-group-structures/parent-child) tree, where a layer can hold several peers, the other active peers at that layer still earn, because the skip only drops the one inactive peer. The inactive member keeps their slot and starts earning in the same position the moment they're set back to Active, with no roster edit required. The same skip behavior is documented for [Upline cascade](/documentation/calculation-strategies/upline-cascade) and [Downline cascade](/documentation/calculation-strategies/downline-cascade). Deleting a collaborator outright is different. When a collaborator is deleted, Siren automatically removes them from every group they belonged to. The `CleanupCollaboratorGroupMembershipsOnCollaboratorDelete` listener responds to the collaborator-deleted event and deletes all of that collaborator's membership rows. It removes membership rows only. It does not delete the groups themselves. After a delete, the deleted collaborator is no longer a member of any group, so subsequent cascades walk the group as if that member never existed, with everyone below them shifting up a layer. The practical difference: suspend a chain member when they're temporarily out and you want them to slot back into the same position later. Delete (or remove) them when the change is permanent and you want the chain to close the gap. ## What you've done You now know how to keep a live cascade group accurate from the admin UI, which endpoints the UI drives behind the scenes, what `CollaboratorGroupMemberMetadataChanged` signals, and how adds, removals, reorders, suspensions, and deletes each play out on the next cascade trigger. A few places to go next: - [Configure cascade payouts](/documentation/getting-started/configure-cascade-payouts): the setup tutorial, if you need to revisit per-layer config. - [Upline cascade](/documentation/calculation-strategies/upline-cascade) and [Downline cascade](/documentation/calculation-strategies/downline-cascade): the per-strategy reference for skip and stop behavior. - [Managing collaborators](/documentation/getting-started/managing-collaborators-affiliates): how status changes (Active, Pending, Inactive) and deletes work on the collaborator side. ## Opportunities Source: https://www.sirenaffiliates.com/documentation/resource-reference/opportunities Accessing and querying Siren opportunity records through the PHP data layer. import CodeTabs from "@/components/content/CodeTabs.astro"; # Opportunities An opportunity in Siren is an ephemeral tracking record created automatically when a visitor clicks an affiliate link or otherwise triggers an attribution-worthy interaction. Opportunities represent the earliest stage of the attribution pipeline. A potential customer touchpoint may eventually lead to an engagement, conversion, and transaction. You will almost never create opportunities manually. They are produced by Siren's trigger strategies in response to site visits, cookie lookups, and other detection mechanisms. The opportunity system handles creation, deduplication, merging, and invalidation through the normal event flow. The primary reason to access the opportunity datastore directly is reading opportunity data for reporting, looking up an opportunity to inspect its status, or tracing the attribution chain backward from a conversion. > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Opportunities are created and managed by trigger strategies that respond to visitor activity events. If you need to support a new type of visitor interaction, register a custom trigger strategy rather than creating opportunity records directly. This ensures the full event pipeline fires and downstream systems (engagements, conversions) respond correctly. ## Accessing opportunity data The opportunity datastore is available through dependency injection or the static facade. Dependency injection is preferred for extension code wired through an initializer. The facade is convenient for standalone scripts, theme files, or other code running outside Siren's container. ```php use Siren\Opportunities\Core\Datastores\Opportunity\Interfaces\OpportunityDatastore; class AttributionTracer { protected OpportunityDatastore $opportunities; public function __construct(OpportunityDatastore $opportunities) { $this->opportunities = $opportunities; } public function getActiveOpportunities(): array { return $this->opportunities->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); } } ``` ```php use Siren\Opportunities\Core\Facades\Opportunities; $active = Opportunities::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); $opportunity = Opportunities::getById(42); ``` ## The opportunity model Each opportunity record is represented by an `Opportunity` model instance. | Field | Type | Description | |---|---|---| | id | int | Primary key | | status | string | `active` or `invalid` | | lastTriggered | DateTime | When the opportunity was most recently triggered | | createdDate | DateTime | When the opportunity was first recorded | | modifiedDate | DateTime | When the record was last updated | Getter methods: `getStatus()`, `getLastTriggered()`, `getCreatedDate()`, `getModifiedDate()`. The opportunity model is deliberately lightweight. It does not carry collaborator or program information. Those associations are established by engagements, which link an opportunity to a collaborator within a program. A single opportunity can have multiple engagements across different programs if the visitor's interaction triggers attribution in more than one context. The `lastTriggered` timestamp updates each time the visitor re-triggers the opportunity (for example, a return visit through the same affiliate link). This allows Siren to track recency without creating duplicate records. ## Available methods This datastore supports all shared methods documented in the [introduction](/documentation/resource-reference/introduction). It also provides one domain-specific method. ### Looking up an opportunity by conversion `getByConversionId` retrieves the opportunity that led to a specific conversion. This is the primary method for tracing the attribution chain backward. Given a conversion, you can find the original opportunity, and from there query the engagements that were active when the conversion occurred. ```php use Siren\Opportunities\Core\Datastores\Opportunity\Interfaces\OpportunityDatastore; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; class ConversionTracer { protected OpportunityDatastore $opportunities; public function __construct(OpportunityDatastore $opportunities) { $this->opportunities = $opportunities; } public function traceConversion(int $conversionId): ?array { try { $opportunity = $this->opportunities->getByConversionId($conversionId); return [ 'opportunityId' => $opportunity->getId(), 'status' => $opportunity->getStatus(), 'firstSeen' => $opportunity->getCreatedDate(), 'lastTriggered' => $opportunity->getLastTriggered(), ]; } catch (RecordNotFoundException $e) { return null; } } } ``` ```php use Siren\Opportunities\Core\Facades\Opportunities; // Trace back from a conversion to its originating opportunity $opportunity = Opportunities::getByConversionId($conversionId); echo $opportunity->getId(); // The opportunity that led to this conversion echo $opportunity->getLastTriggered(); // When the visitor last interacted ``` Throws `RecordNotFoundException` if no opportunity is associated with the given conversion ID. This method is only available on the datastore interface, not on the facade's docblock hints, but it is callable through the facade since the facade delegates all method calls to the underlying datastore instance. If your IDE does not autocomplete it through the facade, you can access it via `Opportunities::getContainedInstance()->getByConversionId($conversionId)` for full type safety. ## OpportunityInvalidated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-attribution/opportunity-invalidated Fires when an existing opportunity is determined to be invalid due to duplication, expiration, or failed validation. # OpportunityInvalidated Not every opportunity makes it through the pipeline. When the system determines that an opportunity is invalid, it fires `OpportunityInvalidated` to clean up the attribution state. This can happen when a duplicate opportunity is detected, when a referral link turns out to be expired, or when any other validation check rejects the opportunity. The event is identified as `opportunity_invalidated` and lives in the `Siren\Opportunities\Core\Events` namespace. ## What does this event carry? The event carries the `Opportunity` model that was invalidated. ```php use Siren\Opportunities\Core\Events\OpportunityInvalidated; $opportunity = $event->getOpportunity(); ``` The opportunity model gives listeners everything they need to identify and clean up related records, including the collaborator, program associations, and the original trigger context. ## What happens when it fires? The `InvalidateOpportunityEngagements` listener marks any engagements that were created for this opportunity as invalid. This prevents those engagements from contributing to conversions, ensuring that invalid referrals never generate obligations or payouts. Invalidation is a terminal state. Once an opportunity is invalidated, its engagements cannot be re-activated. If the same customer interaction turns out to be legitimate, the system creates a new opportunity rather than restoring the invalidated one. See the [Attribution Events overview](/documentation/developer-reference/events-attribution) for how this event fits into the full attribution pipeline. ## OpportunityTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-attribution/opportunity-triggered Fires when a new opportunity is created from a customer referral link, coupon, or tracked touchpoint. # OpportunityTriggered Every attribution flow in Siren starts with an opportunity. When a customer visits a site through an affiliate link, applies a coupon, or interacts with any other tracked touchpoint, the system creates an opportunity record and fires `OpportunityTriggered`. This is the entry point for the entire engagement and conversion pipeline. The event is identified as `opportunity_triggered` and lives in the `Siren\Opportunities\Core\Events` namespace. ## What does this event carry? The event provides two pieces of data: the full `Opportunity` model and a `trigger` string that identifies what caused the opportunity to be created. ```php use Siren\Opportunities\Core\Events\OpportunityTriggered; $opportunity = $event->getOpportunity(); $trigger = $event->getTrigger(); ``` The trigger string tells you the source of the opportunity. A customer clicking an affiliate link produces a different trigger than a customer entering a coupon code at checkout. This distinction matters because engagement trigger strategies use it to decide how to handle the opportunity. ## What happens when it fires? Engagement trigger strategies evaluate the opportunity and decide whether to create engagement records. The `ReferredSiteVisit` strategy, for example, listens to this event and creates engagements for the collaborator whose referral link was used. If the collaborator participates in multiple programs, the strategy may produce multiple engagement records from a single opportunity. If the opportunity is later found to be invalid (due to duplication, expiration, or failed validation), the system fires [OpportunityInvalidated](/documentation/developer-reference/events-attribution/opportunity-invalidated) to clean up any engagements that were created. See the [Attribution Events overview](/documentation/developer-reference/events-attribution) for how this event fits into the full attribution pipeline. ## Pagination, Filtering & Field Selection Source: https://www.sirenaffiliates.com/documentation/resource-reference/pagination-filtering-field-selection How pagination, filtering, search, and field selection work across all Siren API list endpoints. # Pagination, Filtering & Field Selection Every list endpoint in the Siren API shares the same set of cross-cutting query patterns for pagination, filtering, sorting, and field selection. These patterns are implemented through a middleware pipeline that processes the request before it reaches the controller, and an interceptor that wraps the response afterward. This page documents how each pattern works, what parameters are available, and how they combine. Understanding these patterns once means understanding them everywhere — the same parameters behave identically whether you are listing [programs](/documentation/resource-reference/programs), [collaborators](/documentation/resource-reference/collaborators), [transactions](/documentation/resource-reference/transactions), [conversions](/documentation/resource-reference/conversions), or any other resource. These patterns are implemented by `FilterMiddleware`, `PaginationMiddleware`, `FieldResolverMiddleware`, and `ListResponseWrapperInterceptor`. Each list controller wires these into its middleware stack. --- ## Pagination List endpoints use offset-based pagination controlled by two query parameters: | Parameter | Type | Default | Max | Description | |-----------|---------|---------|-----|------------------------------------------| | `number` | integer | `10` | `50`| Number of items per page (page size). | | `offset` | integer | `0` | -- | Zero-based index of the first item to return. | ### Defaults and limits If `number` is omitted, the API returns 10 items. If `number` exceeds 50, the API silently caps it at 50 — you will never receive more than 50 items in a single response regardless of the value you pass. If `offset` is omitted, it defaults to 0 (start from the beginning). The `offset` parameter must be zero or a positive integer. Negative values are rejected with a 400 validation error. ### Page calculation The response envelope includes a `page` field calculated from offset and page size: ``` page = floor(offset / number) + 1 ``` For example, `offset=0, number=10` yields `page=1`. `offset=20, number=10` yields `page=3`. ### Sorting Two additional parameters control result ordering: | Parameter | Type | Default | Description | |-----------|--------|---------|--------------------------------------| | `orderBy` | string | `id` | Column to sort by. | | `order` | string | varies | Sort direction: `ASC` or `DESC`. | The default sort direction varies by resource. Resources that represent configuration (programs, collaborators, program groups, collaborator groups) default to `ASC` (oldest first). Resources that represent activity or time-series data (transactions, conversions, engagements, distributions, obligations, fulfillments, payouts) default to `DESC` (newest first). | Default `ASC` (oldest first) | Default `DESC` (newest first) | |------------------------------------|-------------------------------------| | [Programs](/documentation/resource-reference/programs) | [Transactions](/documentation/resource-reference/transactions) | | [Collaborators](/documentation/resource-reference/collaborators) | [Conversions](/documentation/resource-reference/conversions) | | [Program Groups](/documentation/resource-reference/program-groups) | [Engagements](/documentation/resource-reference/engagements) | | [Collaborator Groups](/documentation/resource-reference/collaborator-groups) | [Distributions](/documentation/resource-reference/distributions) | | | [Distributors](/documentation/resource-reference/distributors) | | | [Obligations](/documentation/resource-reference/obligations) | | | [Fulfillments](/documentation/resource-reference/fulfillments) | | | [Payouts](/documentation/resource-reference/payouts) | Valid `orderBy` values depend on the resource. Most endpoints accept any string column name; some endpoints (e.g., transactions) validate against a whitelist of allowed columns. If an invalid column is provided, the API returns a 400 error. --- ## Filtering List endpoints support two filtering mechanisms: exact-match filters and text search. Both are processed by `FilterMiddleware` before the controller runs. ### Exact-match filters Each endpoint declares which columns support filtering. Pass the column name as a query parameter with the desired value: ``` GET /programs?status=active GET /collaborators?email=alice@example.com GET /engagements?collaboratorId=42&programId=7 ``` Multiple filter parameters are combined with AND logic — all conditions must match. To match any of several values, pass comma-separated values (which generates an IN clause): ``` GET /programs?status=active,inactive ``` This returns programs whose status is either `active` or `inactive`. The API splits the value on commas and generates an `IN (...)` SQL clause. ### Common filterable columns by resource | Resource | Filterable Columns | |----------------|-------------------------------------------------------------| | [Programs](/documentation/resource-reference/programs) | `status`, `incentiveType`, `incentiveResolverType`, `units` | | [Collaborators](/documentation/resource-reference/collaborators) | `status`, `fullName`, `nickname`, `email` | | [Collaborator Groups](/documentation/resource-reference/collaborator-groups) | `structure` | | [Engagements](/documentation/resource-reference/engagements) | `status`, `collaboratorId`, `programId`, `opportunityId` | | [Transactions](/documentation/resource-reference/transactions) | `status` | | [Conversions](/documentation/resource-reference/conversions) | `status`, `distributorId` | | [Distributions](/documentation/resource-reference/distributions) | `status`, `distributorId` | | [Distributors](/documentation/resource-reference/distributors) | `status` | | [Obligations](/documentation/resource-reference/obligations) | `status` | | [Fulfillments](/documentation/resource-reference/fulfillments) | `status` | | [Payouts](/documentation/resource-reference/payouts) | `status` | Each resource reference page lists its own filterable columns. ### Text search (`?s=`) Some endpoints support free-text search via the `?s=` parameter. When provided, the API generates `LIKE '%term%'` clauses across all searchable columns for that resource. Searchable columns are combined with OR logic — a match in any column returns the record. ``` GET /programs?s=referral GET /collaborators?s=john ``` For programs, `?s=` searches across `name` and `description`. For collaborators, it searches `fullName`, `nickname`, and `email` — and additionally searches collaborator alias codes (coupon codes, referral slugs, etc.) via a secondary lookup. Not all resources support `?s=`. Resources with no text-searchable columns (transactions, engagements, distributions, obligations, fulfillments, payouts) do not respond to the `?s=` parameter. | Resource | Searchable Columns | |----------------|---------------------------------------| | [Programs](/documentation/resource-reference/programs) | `name`, `description` | | [Collaborators](/documentation/resource-reference/collaborators) | `fullName`, `nickname`, `email` + alias codes | | [Collaborator Groups](/documentation/resource-reference/collaborator-groups) | `name`, `description` | | Others | _(no text search)_ | ### Combining filters and search Filters and search combine with AND logic at the top level. The API builds a compound query structure: - All exact-match filters form an AND group (every filter must match). - All search clauses form an OR group (any searchable column can match). - The AND group and the OR group are combined together — a record must satisfy all exact filters AND match at least one search column. ``` GET /collaborators?status=active&s=john ``` This returns collaborators who are `active` AND whose name, nickname, email, or alias code contains "john". ### Junction table filters Some endpoints support filtering by related resource IDs through junction tables. These are not simple column filters — they resolve the relationship first and then filter the primary resource by matching IDs. ``` GET /programs?programGroupId=3 # Programs in group 3 GET /collaborators?programId=7 # Collaborators enrolled in program 7 GET /collaborators?distributorId=12 # Collaborators assigned to distributor 12 ``` When no matching records exist in the junction table, the endpoint returns an empty result set (not an error). --- ## Field Selection Every list and detail endpoint supports field selection via the `?fields=` query parameter. This controls which fields appear in each item of the response. Fields are resolved dynamically through a field resolver registry — each resource registers its own set of available fields. ### The `fields` parameter Pass a comma-separated list of field names: ``` GET /programs?fields=id,name,status GET /collaborators?fields=id,fullName,email,status ``` The middleware parses the CSV string, validates each field against the registry of available resolvers, and silently drops any unrecognized field names. The controller then resolves only the requested fields for each record. ### Required vs. optional On some endpoints (programs, collaborators, program groups, collaborator groups), `fields` is a **required** parameter. Omitting it returns a 400 validation error. On other endpoints (transactions, engagements, conversions, distributions, fulfillments, payouts), `fields` is optional and the endpoint falls back to a set of default fields when omitted. When `fields` is optional, the endpoint defines a `DEFAULT_FIELDS` constant. For example, the transactions endpoint defaults to `['id', 'status', 'dateCreated']` and the engagements endpoint defaults to `['id', 'opportunityId', 'programId', 'collaboratorId', 'score', 'status', 'dateCreated', 'dateModified']`. ### How field resolution works Field resolution is a two-step process: 1. **Registration:** When a list endpoint handles a request, it broadcasts an event (e.g., `ProgramResolverRegistryInitiated`). Listeners respond by registering resolver functions — closures that accept a data model and return the field value. Core fields map directly to model properties. Extended fields perform additional queries or computations (e.g., `collaboratorCount` runs an aggregate query, `collaborators` loads related records). 2. **Resolution:** The controller iterates over each record and each requested field, calling the registered resolver function to produce the value. The result is an array of associative arrays, one per record, with only the requested fields. This means that requesting expensive fields (like `collaborators` which loads a full list of related records) has a real performance cost. Request only the fields you need. ### Core vs. extended fields Each resource has two categories of fields: - **Core fields** map directly to columns on the resource's database table. They are cheap to resolve — just reading a property from the already-loaded model. Examples: `id`, `name`, `status`, `dateCreated`. - **Extended fields** require additional queries or computation. They are registered by separate listeners and may involve joins, subqueries, or cross-datastore lookups. Examples: `collaboratorCount`, `collaborators`, `totalValue`, `distributorName`. The resource reference pages list all available fields and note which are extended. ### Detail endpoints (get by ID) Single-resource endpoints (`GET /programs/{id}`, `GET /collaborators/{id}`, etc.) also support `?fields=`. The behavior is identical — pass a comma-separated list, and the response includes only those fields. When omitted, detail endpoints return their full set of default fields. ``` GET /programs/42?fields=id,name,engagementTypes,transactionCompilers ``` Detail endpoints do not use `PaginationMiddleware` or `ListResponseWrapperInterceptor` — they return a flat JSON object, not a paginated envelope. --- ## Response Format ### List response envelope All paginated list endpoints wrap their response in a standard JSON envelope with five fields: ```json { "items": [ { "id": 1, "name": "Partner Commissions", "status": "active" }, { "id": 2, "name": "Referral Rewards", "status": "active" } ], "total": 47, "page": 1, "perPage": 10, "totalPages": 5 } ``` | Field | Type | Description | |--------------|---------|----------------------------------------------------------------| | `items` | array | Array of objects, each containing only the requested fields. | | `total` | integer | Estimated total count of records matching the current filters. | | `page` | integer | Current page number (1-based, derived from `offset`). | | `perPage` | integer | Page size used for this request. | | `totalPages` | integer | Total pages available: `ceil(total / perPage)`. | This wrapping is applied by `ListResponseWrapperInterceptor` after the controller has set the response body. The interceptor reads `number` and `offset` from the request and `x-siren-estimated-count` from the response header to compute the pagination metadata. ### The `x-siren-estimated-count` header Every list response includes a custom response header: ``` x-siren-estimated-count: 47 ``` This header contains the total number of records matching the current filters (before pagination). The envelope's `total` field is sourced from this header. The header is also included in the `Access-Control-Expose-Headers` response header, making it accessible to browser-based JavaScript clients via `response.headers.get('x-siren-estimated-count')`. The count is called "estimated" because it is computed at query time and may shift between requests if records are being created or deleted concurrently. ### Empty results When no records match the query, the API returns HTTP 200 with an empty items array: ```json { "items": [], "total": 0, "page": 1, "perPage": 10, "totalPages": 1 } ``` The API never returns 404 for an empty list. A 404 is only returned when a specific resource ID does not exist (on detail endpoints). ### Endpoints without the envelope A small number of list endpoints return raw JSON arrays without the pagination wrapper. These are typically configuration or metadata endpoints like `GET /event-types` and `GET /programs/currency-types`. These endpoints return simple lists where pagination is unnecessary. --- ## Examples ### Basic pagination Fetch the first page of 10 programs: ``` curl "https://api.example.com/siren/v1/programs?fields=id,name,status" \ -H "Authorization: Bearer $TOKEN" ``` Fetch page 3 (items 21-30): ``` curl "https://api.example.com/siren/v1/programs?fields=id,name,status&number=10&offset=20" \ -H "Authorization: Bearer $TOKEN" ``` Fetch 25 items per page (maximum 50): ``` curl "https://api.example.com/siren/v1/programs?fields=id,name,status&number=25" \ -H "Authorization: Bearer $TOKEN" ``` ### Filtering by status ``` curl "https://api.example.com/siren/v1/programs?fields=id,name,status&status=active" \ -H "Authorization: Bearer $TOKEN" ``` ### Multi-value filter Fetch programs that are either active or inactive (excludes deleted): ``` curl "https://api.example.com/siren/v1/programs?fields=id,name,status&status=active,inactive" \ -H "Authorization: Bearer $TOKEN" ``` ### Text search Search for collaborators whose name, email, or alias code contains "smith": ``` curl "https://api.example.com/siren/v1/collaborators?fields=id,fullName,email,status&s=smith" \ -H "Authorization: Bearer $TOKEN" ``` ### Combining filters and search Active collaborators matching "smith": ``` curl "https://api.example.com/siren/v1/collaborators?fields=id,fullName,email&status=active&s=smith" \ -H "Authorization: Bearer $TOKEN" ``` ### Sorting Collaborators sorted by name descending: ``` curl "https://api.example.com/siren/v1/collaborators?fields=id,fullName,status&orderBy=fullName&order=DESC" \ -H "Authorization: Bearer $TOKEN" ``` Transactions sorted by date created ascending (overriding the default DESC): ``` curl "https://api.example.com/siren/v1/transactions?fields=id,status,dateCreated&orderBy=dateCreated&order=ASC" \ -H "Authorization: Bearer $TOKEN" ``` ### Field selection on a detail endpoint Get a single program with only specific fields: ``` curl "https://api.example.com/siren/v1/programs/42?fields=id,name,collaboratorCount,engagementsCount" \ -H "Authorization: Bearer $TOKEN" ``` Response (flat object, no envelope): ```json { "id": 42, "name": "Partner Commissions", "collaboratorCount": 156, "engagementsCount": 1203 } ``` ### Junction table filtering Collaborators enrolled in program 7: ``` curl "https://api.example.com/siren/v1/collaborators?fields=id,fullName,status&programId=7" \ -H "Authorization: Bearer $TOKEN" ``` Programs in program group 3: ``` curl "https://api.example.com/siren/v1/programs?fields=id,name,status&programGroupId=3" \ -H "Authorization: Bearer $TOKEN" ``` ### Iterating all pages ``` #!/bin/bash PAGE_SIZE=25 OFFSET=0 TOTAL_PAGES=1 while [ "$OFFSET" -lt "$((TOTAL_PAGES * PAGE_SIZE))" ]; do RESPONSE=$(curl -s \ "https://api.example.com/siren/v1/collaborators?fields=id,fullName,status&number=$PAGE_SIZE&offset=$OFFSET" \ -H "Authorization: Bearer $TOKEN") TOTAL_PAGES=$(echo "$RESPONSE" | jq '.totalPages') ITEMS=$(echo "$RESPONSE" | jq '.items') # Process items... echo "$ITEMS" OFFSET=$((OFFSET + PAGE_SIZE)) done ``` ### Reading the estimated count header ``` curl -s -D - \ "https://api.example.com/siren/v1/programs?fields=id,name&status=active" \ -H "Authorization: Bearer $TOKEN" \ | grep -i x-siren-estimated-count # x-siren-estimated-count: 47 ``` --- ## Quick Reference | Parameter | Type | Default | Applies To | Description | |------------|---------|----------------|----------------|-----------------------------------------------------| | `fields` | string | varies by endpoint | List + Detail | Comma-separated field names to include in response. | | `number` | integer | `10` | List only | Page size (max 50). | | `offset` | integer | `0` | List only | Zero-based starting position. | | `orderBy` | string | `id` | List only | Column to sort by. | | `order` | string | `ASC` or `DESC`| List only | Sort direction (default varies by resource). | | `s` | string | -- | List only | Free-text search across searchable columns. | | _(column)_ | string | -- | List only | Exact-match filter (comma-separated for multi-value).| --- ## See Also - See the individual resource reference pages for available fields and filterable columns per endpoint. ## Parent-child structure Source: https://www.sirenaffiliates.com/documentation/collaborator-group-structures/parent-child A tree of collaborators where one parent can have many children, and cascades walk the tree from the trigger. Parent-child is a [collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) that arranges members into a tree. Each member optionally points at a parent, and one parent can have many children, so the group fans out instead of forming a single line. ## How it works Every member of a parent-child group carries a `parentCollaboratorId` in their metadata. That field is the entire structure. Members whose `parentCollaboratorId` is null, or points at a collaborator who isn't in the group, are roots. A parent-child group can have more than one root, so you're really managing a forest. When a [cascade](/documentation/general/what-is-a-cascade) runs from a triggering collaborator, it walks the tree in one of two directions. Upline means walking parent links toward the root. Downline means walking the children, then their children, and so on. Layer 1 is the immediate parent (upline) or the immediate children (downline). Layer 2 is the grandparent or the grandchildren. Layer 5 is the deepest you can configure. A cascade only ever walks the triggering collaborator's own tree. It follows parent and child links out from the trigger, so it never crosses into a sibling tree in the same forest. The shape matters most on the downline side. Layer N in a downline cascade can include many peers, every cousin at that depth from the trigger. If a manager has three direct reports and each of those reports has two reports of their own, a downline cascade from the manager hits three collaborators at layer 1 and six collaborators at layer 2. Every one of them is credited at the per-layer rate. Upline is always a single chain, because each member has at most one parent. Because each layer can be wide, a deep cascade on a large tree can credit a lot of people on a single sale, so set the per-layer points and the depth with your total payout in mind. The admin UI shows the tree as an indented list. Drag a row vertically to change its place among its siblings. Drag horizontally during the vertical drag to change its depth: indent to nest the row under a new parent, outdent to lift it up the tree. A per-row parent dropdown is also available for keyboard and screen-reader users, so you can reparent without dragging. Membership is dynamic. You can add a member with a parent pointer, drop the parent later (the child becomes a root), or reparent at any time. The admin UI prevents cycles. You can't drop a parent onto one of its own descendants, because the resulting loop would make "upline" undefined. The API does not validate the full graph, so a payload that points two members at each other can store a cycle. The cascade walkers defend against cycles at runtime with a visited set, so the walk stops instead of looping forever, but you get a partial cascade. Check your parent pointers before you PUT them. Suspended members earn nothing and are skipped, but they do not break the tree. A downline cascade still walks through a suspended parent to the people below it, and those people are credited at their own depth. Layers are not renumbered, so a suspended member at layer 2 leaves layer 2 unpaid while everyone below stays at their own layer. Deleting a collaborator is different. Siren removes a deleted collaborator from every group, so any children that pointed at them now have a parent outside the group and become roots, which detaches that branch from a cascade running through the deleted member. Reparent the children first if you want the branch to keep earning. ## When to use it Reach for parent-child whenever one person has direct authority over (or earns an override from) more than one downline collaborator. Brokerage hierarchies fit this shape, where a real estate or insurance principal oversees many agents and each of those agents oversees their own. The same branching shows up in an ordinary org chart, where a manager owns many direct reports who in turn lead teams of their own, and in a channel or partner program, where a partner manager oversees several partners who each manage their own sub-partners. What ties these together is the branching authority underneath them: anywhere one person sits above multiple collaborators and you want all of them counted under that person, you have the branching authority that parent-child is built to track. If your structure is strictly linear, where one person sits above exactly one, who sits above exactly one, use [linear chain](/documentation/collaborator-group-structures/linear-chain) instead. Linear chain is simpler to configure, and its drag-reorder UI is more direct than a tree. Parent-child only earns its complexity when the tree actually branches. If there is no hierarchy at all and every member is an equal peer, use [flat](/documentation/collaborator-group-structures/flat). ## Configuration In the admin UI, [create a collaborator group](/documentation/getting-started/create-a-collaborator-group), set the structure to Parent-Child, and add members. Use the drag handles to nest or reorder rows, or pick a parent from the per-row dropdown. Save the group when the tree matches the relationships you want. In the API, PUT to `/collaborator-groups/{id}/members` with each member carrying its parent in metadata: ```json { "members": [ { "collaboratorId": 101, "metadata": { "parentCollaboratorId": null } }, { "collaboratorId": 102, "metadata": { "parentCollaboratorId": 101 } }, { "collaboratorId": 103, "metadata": { "parentCollaboratorId": 101 } }, { "collaboratorId": 104, "metadata": { "parentCollaboratorId": 102 } } ] } ``` That payload describes a group with one root (101), two children under it (102 and 103), and one grandchild under 102 (104). Omitting `parentCollaboratorId` or setting it to null marks a root. Pointing it at a collaborator id outside the group also marks a root. The missing parent is treated as "not in this tree." This PUT replaces the group's full member list. Any member you leave out of the body is removed from the group, which also turns that member's children into roots, so send the whole tree each time. To change one member without resending everything, use the add and remove member endpoints instead. Once the group is saved, bind it to a [program](/documentation/calculation-strategies/upline-cascade) or distributor and pick a cascade calculation. Upline cascade walks toward the roots from whoever triggered the engagement. [Downline cascade](/documentation/calculation-strategies/downline-cascade) walks toward the leaves. The per-layer points you configure on the calc decide how much each layer earns, and 0 on any layer stops the cascade there. ## Payment Events Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments Domain events for obligations, fulfillments, payouts, and transaction records. # Payment Events Payment events track the financial side of Siren's pipeline, from the creation of a transaction through obligation issuance to the final payout. These events span three domains: transactions (`Siren\Transactions\Core\Events`), obligations (`Siren\Obligations\Core\Events`), and fulfillments (`Siren\Fulfillments\Core\Events`). Together they form the chain that turns a conversion into money owed, and money owed into money paid. ## Transaction creation Transaction events fire when financial records are created. The flow begins with a mutable request event that lets listeners shape transaction data before it is persisted, followed by a read-only notification once the record is committed. [TransactionCreateRequested](/documentation/developer-reference/events-payments/transaction-create-requested) fires before a transaction is written to the database. Because it is mutable, listeners can add, remove, or transform transaction details before the record is created. This is the primary extension point for customizing how transaction data is assembled. [TransactionCreated](/documentation/developer-reference/events-payments/transaction-created) fires after the transaction has been persisted. Listeners use it for side effects that depend on a committed record, such as logging, syncing to external systems, or updating analytics. ## Obligation lifecycle An obligation represents a debt the business owes a collaborator as a result of a conversion. Obligation events track these commitments from creation through settlement. [ObligationIssued](/documentation/developer-reference/events-payments/obligation-issued) fires when an obligation is created for a conversion. It carries an optional conversion ID that links the obligation back to the conversion that caused it, since some creation paths like manual adjustments may not originate from a conversion. [ObligationCompleted](/documentation/developer-reference/events-payments/obligation-completed) fires when an obligation has been fulfilled through a payout. At this point the obligation's status has been set to fulfilled and the payout ID has been written to the obligation record. ## Fulfillment and payout processing Fulfillment events fire during the payout generation process, when accumulated obligations are compiled into concrete payment records. [FulfillmentCreated](/documentation/developer-reference/events-payments/fulfillment-created) fires when a new fulfillment batch is created. A fulfillment represents a single payout run that groups obligations together for disbursement. [FulfillmentStatusChanged](/documentation/developer-reference/events-payments/fulfillment-status-changed) fires when a fulfillment transitions between statuses. This event is useful for triggering notifications or external integrations as a fulfillment moves through its lifecycle. [PayoutCreated](/documentation/developer-reference/events-payments/payout-created) fires when an individual payout record is created within a fulfillment. A payout is a single disbursement line item specifying an amount, currency, and collaborator. A fulfillment that pays three collaborators will produce three separate events. [PayoutPaid](/documentation/developer-reference/events-payments/payout-paid) fires when a payout is marked as disbursed. This is the terminal event in the payment flow. Listeners use it to trigger payment gateway confirmations, send receipt emails, or update external accounting systems. ## Payout Bulk Actions Source: https://www.sirenaffiliates.com/documentation/resource-reference/payouts/bulk-actions Performs batch operations on multiple payouts to mark them as paid or unpaid. ### Payout Bulk Actions `POST /siren/v1/payouts/bulk` Performs an action on multiple payouts at once. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `action` | string | Yes | One of: `markPaid`, `markUnpaid` | | `ids` | integer[] | Yes | Array of payout IDs to act upon | #### Action Behaviors | Action | Effect | Events | |---|---|---| | `markPaid` | Sets status to `paid`, sets `datePaid` to current timestamp | Broadcasts `PayoutPaid` per record | | `markUnpaid` | Sets status to `unpaid`, clears `datePaid` to null | -- | Payouts already in the target status are skipped. Non-existent IDs are silently skipped. #### Example ```json { "action": "markPaid", "ids": [20, 21] } ``` ```json { "success": true, "affected": 2 } ``` ### State Transition Rules Payout status is a simple toggle between `paid` and `unpaid`. The `datePaid` timestamp is set when marking as paid and cleared when marking as unpaid. Both the bulk action endpoint and the export endpoint (when `markAsPaid` is true) use the same underlying mechanism to transition payout status and broadcast events. ## PayoutCreated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/payout-created Fires when an individual payout record is created within a fulfillment. # PayoutCreated Within a fulfillment batch, each collaborator who is owed money gets an individual payout record. When one of these records is created, Siren fires `PayoutCreated`. A single fulfillment paying three collaborators will produce three separate `PayoutCreated` events, one per payout. The event is identified as `payout_created` and lives in `Siren\Fulfillments\Core\Events\PayoutCreated`. ## What does this event carry? The event provides the `Payout` model, which contains the disbursement amount, currency, and the collaborator who will receive the payment. ```php use Siren\Fulfillments\Core\Events\PayoutCreated; public function handle(Event $event): void { $payout = $event->getPayout(); $amount = $payout->getValue(); $currency = $payout->getCurrency(); $collaboratorId = $payout->getCollaboratorId(); } ``` ## When would you use it? Listeners subscribe to this event to react when a specific collaborator's payout is queued. Common use cases include notifying the collaborator that a payment is being prepared, validating payment details before disbursement, or syncing payout records to external payment platforms. Once the payout is actually disbursed, the system fires [PayoutPaid](/documentation/developer-reference/events-payments/payout-paid) as the terminal event in the payment flow. See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## PayoutPaid Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/payout-paid The terminal event in the payment flow. Fires when a payout is marked as disbursed. # PayoutPaid `PayoutPaid` is the terminal event in Siren's payment pipeline. It fires when a payout record is marked as disbursed, meaning the money has been sent (or at least recorded as sent) and the payout's status reflects that. Everything upstream, from the original commerce event through conversion, obligation, fulfillment, and payout creation, has led to this point. The event is identified as `payout_paid` and lives in `Siren\Fulfillments\Core\Events\PayoutPaid`. ## What does this event carry? The event provides the `Payout` model, which contains the collaborator ID, disbursement amount, and currency. ```php use Siren\Fulfillments\Core\Events\PayoutPaid; public function handle(Event $event): void { $payout = $event->getPayout(); $collaboratorId = $payout->getCollaboratorId(); $amount = $payout->getValue(); } ``` ## When would you use it? This is the right event for anything that should happen after money changes hands. Listeners commonly use it to trigger payment gateway confirmations, send receipt emails to collaborators, or push completed payment records to external accounting systems. Because this event represents the end of the payment flow, it is also a natural point for reconciliation logic that verifies the payout amount matches what was expected from the obligation. See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## Payouts Source: https://www.sirenaffiliates.com/documentation/resource-reference/payouts Individual payment records within fulfillments — data model, status lifecycle, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; # Payouts A payout is a single line item representing the amount owed to one [collaborator](/documentation/resource-reference/collaborators) within a [fulfillment](/documentation/resource-reference/fulfillments) batch. Each payout records the currency, value, and payment status for one collaborator's share of a fulfillment run. ## The payout object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Value | `value` | `getValue()` | integer | Amount owed in smallest currency unit (e.g., cents) | | Currency | `currency` | `getCurrency()` | string | Currency code (e.g., `USD`) | | Status | `status` | `getStatus()` | string | Current status: `paid` or `unpaid` | | Collaborator | `collaboratorId` | `getCollaboratorId()` | integer | ID of the collaborator receiving this payout | | Fulfillment | `fulfillmentId` | `getFulfillmentId()` | integer | ID of the parent fulfillment batch | | Date paid | `datePaid` | `getPaidDate()` | datetime or null | When the payout was marked as paid | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the record was created | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the record was last modified | ## Status lifecycle | Status | Description | |---|---| | `unpaid` | The payout has been created but payment has not yet been made. The datePaid timestamp is null. | | `paid` | The payout has been marked as paid and the datePaid timestamp is set. Can be toggled back to unpaid if needed. | ## Accessing payout data ```bash # List payouts for a collaborator curl -X GET "https://your-site.com/wp-json/siren/v1/payouts?collaboratorId=42&fields=id,value,currency,status,datePaid" \ -H "Authorization: Bearer YOUR_TOKEN" # List payouts in a fulfillment with collaborator details curl -X GET "https://your-site.com/wp-json/siren/v1/payouts?fulfillmentId=12&fields=id,value,status,collaboratorName,collaboratorEmail" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Fulfillments\Core\Datastores\Payout\Interfaces\PayoutDatastore; class PayoutReport { protected PayoutDatastore $payouts; public function __construct(PayoutDatastore $payouts) { $this->payouts = $payouts; } public function getPayoutsForCollaborator(int $collaboratorId): array { return $this->payouts->andWhere([ ['column' => 'collaboratorId', 'operator' => '=', 'value' => $collaboratorId] ]); } public function getPayoutsForFulfillment(int $fulfillmentId): array { return $this->payouts->andWhere([ ['column' => 'fulfillmentId', 'operator' => '=', 'value' => $fulfillmentId] ]); } } ``` ```php use Siren\Fulfillments\Core\Facades\Payouts; // Get all payouts for a specific collaborator $payouts = Payouts::andWhere([ ['column' => 'collaboratorId', 'operator' => '=', 'value' => $collaboratorId] ]); // Get all payouts within a specific fulfillment $payouts = Payouts::andWhere([ ['column' => 'fulfillmentId', 'operator' => '=', 'value' => $fulfillmentId] ]); $payout = Payouts::getById(55); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Fulfillments and payouts are normally created by the fulfillment generation pipeline in response to admin actions. If you find yourself creating these records manually, consider whether you should be triggering the generation process instead, which ensures obligations are correctly resolved and all downstream events fire. ## PHP domain methods The payout datastore supports all shared methods documented in the [introduction](/documentation/resource-reference/introduction). It does not have additional domain-specific methods beyond standard CRUD. ### Querying payouts by status and fulfillment A common pattern is finding all unpaid payouts for a given fulfillment, or calculating the total value of a fulfillment's payouts. ```php use Siren\Fulfillments\Core\Facades\Payouts; // Find unpaid payouts in a fulfillment $unpaid = Payouts::andWhere([ ['column' => 'fulfillmentId', 'operator' => '=', 'value' => $fulfillmentId], ['column' => 'status', 'operator' => '!=', 'value' => 'paid'] ]); // Calculate total payout value for a fulfillment $payouts = Payouts::andWhere([ ['column' => 'fulfillmentId', 'operator' => '=', 'value' => $fulfillmentId] ]); $total = array_sum(array_map(fn($p) => $p->getValue(), $payouts)); ``` ## Extended fields (REST only) | Field | Type | Description | |---|---|---| | `collaboratorName` | string | Display name of the collaborator | | `collaboratorEmail` | string | Email address of the collaborator | ## Relationships Each payout belongs to a single [collaborator](/documentation/resource-reference/collaborators) through `collaboratorId`. When payouts are exported to CSV, the export endpoint resolves the collaborator's name and email for inclusion in the output. Each payout belongs to a [fulfillment](/documentation/resource-reference/fulfillments) batch through `fulfillmentId`. The fulfillment's extended fields (`payoutCount`, `totalValue`, etc.) are computed aggregates over its child payouts. Payouts connect back to [obligations](/documentation/resource-reference/obligations) indirectly. The generation process that creates payouts also links each source obligation to its payout via the obligation's `payoutId` field. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## Percentage of Transaction Source: https://www.sirenaffiliates.com/documentation/incentive-structures/percentage-of-transaction An incentive structure that pays a percentage of the transaction value as commission. The most common incentive structure. Percentage of Transaction is an incentive structure that pays a fixed percentage of a [transaction's](/documentation/general/what-are-transactions) value as the commission. It's the most common incentive structure in affiliate marketing because it scales naturally with sale size. When a customer converts, Siren multiplies the configured percentage by the qualifying transaction value and creates an obligation for that amount. [Line item filters](/documentation/general/line-item-filters) control which parts of the transaction count toward the total, so you can exclude shipping, taxes, specific product categories, or anything else you don't want to pay commission on. ## How it works If the incentive is set to 10% and a customer buys $250 worth of products (after line item filters are applied), the commission is $25. If they buy $1,000 worth, the commission is $100. Larger carts generate larger payouts without any reconfiguration. If you set filters to exclude shipping and taxes, a $250 order with $20 shipping and $15 tax would calculate commission against $250, not $285. Filters are how you make sure you're paying out on margin you actually earned. ## Where this works This structure fits any program where you want collaborators' payouts to scale with sale size. It's the default choice for standard affiliate programs, creator royalty programs, and revenue-share arrangements. The [basic affiliate program](/recipes/basic-affiliate-program) recipe uses this structure for a conventional percentage-based commission. The [product royalty program](/recipes/product-royalty-program) recipe applies it to pay creators a percentage when their own products sell, so creators earn more when their products are in higher-value carts. ## When to avoid this For low-margin products, a percentage commission can eat your margin before you break even. A fixed fee gives you predictable unit economics in that case. Use [Fixed Per Transaction](/documentation/incentive-structures/fixed-per-transaction) or [Fixed Per Product](/documentation/incentive-structures/fixed-per-product) instead. It's also a poor fit for lead-generation programs where no transaction value exists at the moment you want to pay. If you're paying affiliates for qualified leads or form submissions, there's nothing to take a percentage of. Fixed Per Transaction handles that cleanly. Heavy discounting is another trap. If your store runs frequent sales, percentage commissions fluctuate with the discounted price, which can feel unfair to affiliates who drove the sale. A flat fee avoids that volatility. ## Performance Weighted Pool (Distribution Structure) Source: https://www.sirenaffiliates.com/documentation/distribution-structures/performance-weighted-pool A distribution structure where the reward pool is divided among collaborators proportionally based on each one's metric score relative to the total. The Performance Weighted Pool is a distribution structure where the reward pool is divided among all collaborators who earned metric scores during the distribution period, proportional to each collaborator's score relative to the total. Collaborators who contribute more receive a larger share. When a distribution triggers, Siren tallies the metric scores for every collaborator, calculates each one's percentage of the total, and applies that percentage to the reward pool. A collaborator with 40% of the total metric score receives 40% of the reward pool. Every collaborator with a non-zero score receives something, but higher performers receive more. ## How the reward pool is calculated The reward pool for a distribution is based on a percentage of revenue collected since the last distribution. You configure this percentage when setting up the [distributor](/documentation/general/what-are-distributors). For example, if you set the pool to 15% and your site earned $40,000 since the last distribution, the reward pool for that period is $6,000. With the Performance Weighted Pool, that $6,000 is divided based on each collaborator's share of the total metric score. If three collaborators have scores of 500, 300, and 200 (totaling 1,000), they receive $3,000, $1,800, and $1,200 respectively. ## How metric scores work in this structure Each distributor is configured with [tracking events](/documentation/general/what-are-distributors#tracking-events-and-metric-values) that define what it measures. As these events happen during the distribution period, Siren accumulates a metric score for each collaborator. Each event type has a configurable point value. In the Performance Weighted Pool, these scores directly determine payout size. The point values you assign to different tracking events shape what the distributor rewards most. If you set course completions to 10 points and lesson completions to 1 point, a collaborator whose students complete full courses will outweigh one whose students only finish individual lessons. This lets you signal which behaviors you value through the point configuration. Cascade-emitted scores feed the pool the same way direct scores do. If the distributor is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, a single trigger emits one metric per credited layer at the per-layer score you configured. Those scores accumulate into each collaborator's running total alongside any Fixed-calc metrics the same distributor records, and the pool divides proportionally over the combined score at distribution time. With per-layer values of 100/50/25, a single triggering sale moves the chain's upline by 4:2:1 toward whatever share they end up with. ## Where this works This is the most common distribution structure for revenue-sharing programs. It naturally rewards contribution while still ensuring that every active collaborator receives something. The [content creator profit share](/recipes/content-creator-profit-share) recipe uses this structure to divide a percentage of site revenue among bloggers based on traffic to their posts. A blogger who drives 10,000 visits earns proportionally more than one who drives 1,000, but both receive a share. The [instructor revenue share](/recipes/instructor-revenue-share) recipe applies the same approach to course platforms, where instructors earn based on student engagement with their courses. The [management incentive plan](/recipes/management-incentive-plan) uses it for quarterly management bonuses tied to team performance. This structure works especially well when you want to: - Reward proportionally without creating a winner-take-all dynamic - Motivate collaborators to increase their contributions over time - Run ongoing revenue-share arrangements where everyone who participates earns based on their output ## When to avoid this If you want to create a competitive bonus where only the top performer gets paid, use [Top Score Wins](/documentation/distribution-structures/top-score-wins) instead. The Performance Weighted Pool always pays every contributing collaborator, which dilutes the incentive for extreme performance. If contribution level doesn't matter and you just want equal shares for participation, the [Shared Engagement Pool](/documentation/distribution-structures/shared-engagement-pool) is simpler and communicates that expectation more clearly. ## Comparison with the program-level structure The [program-level Performance Weighted Pool](/documentation/program-structures/performance-weighted-pool) works the same way conceptually, but operates on a per-transaction basis. When a customer converts, the reward for that transaction is divided proportionally among collaborators based on their engagement scores with that customer. The distribution-level version operates over a time period instead, dividing the accumulated reward pool based on metric scores accumulated across all activity during the period. ## Performance Weighted Pool (Program Structure) Source: https://www.sirenaffiliates.com/documentation/program-structures/performance-weighted-pool A program structure where the reward for a conversion is split among engaged collaborators proportional to their engagement scores with the customer. The Performance Weighted Pool is a [program](/documentation/general/what-are-programs) structure where the reward for a single conversion is divided among every [collaborator](/documentation/general/what-is-a-collaborator) who engaged with the customer, proportional to each one's [engagement](/documentation/general/what-is-an-engagement) score. Every engaged collaborator receives something, but higher scores mean larger shares. When a customer converts, Siren tallies the engagement scores each collaborator accumulated with that customer, calculates each one's percentage of the total, and applies that percentage to the conversion reward. ## How it works If a customer converts on a $500 sale with a 20% commission, the reward for that [conversion](/documentation/general/what-is-a-conversion) is $100. If three collaborators have engagement scores of 500, 300, and 200 (totaling 1,000), they receive $50, $30, and $20 respectively. The point values assigned to different engagement event types determine what "performance" actually means. A program that weights webinar attendance at 10 points and social clicks at 1 point will pay out very differently than one that weights them equally. ## How engagement scores feed the split Cascade-emitted engagement scores feed the split the same way direct scores do. If the program is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, a single trigger credits one engagement per layer at the per-layer score you configured. Those scores count toward each collaborator's engagement total for the converting customer alongside any scores they earned directly, and the proportional split runs over the combined total. A collaborator who only ever receives cascade layer-points still earns a weighted share even though they never personally engaged the customer. ## Where this works This is the most common structure for programs where several collaborators typically touch a customer and you want their payouts to reflect their relative contribution. It avoids winner-take-all dynamics while still rewarding high performers more than casual contributors. The [split commission program](/recipes/split-commission-program) recipe uses this structure to divide commissions among contributing affiliates based on their engagement scores. The [blogger revenue program](/recipes/blogger-revenue-program) recipe applies it to content creators driving traffic, so bloggers who generate more engaged readers earn proportionally more per sale. ## When to avoid this If you want a single winner per conversion (a competitive dynamic rather than a collaborative one), use [Top Score Wins](/documentation/program-structures/top-score-wins). If you don't care about relative contribution and just want every engaged collaborator to receive an equal share, use the simpler [Shared Engagement Pool](/documentation/program-structures/shared-engagement-pool). This structure also assumes you have meaningful engagement data to weight on. If your program only tracks one event type with one point value, the weighted calculation collapses back into an even split and you may as well use the Shared Engagement Pool. ## Comparison with the distribution-level structure The [distribution-level Performance Weighted Pool](/documentation/distribution-structures/performance-weighted-pool) works the same way conceptually but operates over a time period instead of per conversion. The program-level version divides a single conversion reward among engaged collaborators. The distribution-level version divides an accumulated reward pool based on metric scores tallied across all activity during the period. ## Program Groups Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups Program groups in Siren — data model, sorting strategies, REST API, and PHP data access for program priority resolution. import CodeTabs from "@/components/content/CodeTabs.astro"; # Program Groups A program group collects one or more [programs](/documentation/resource-reference/programs) under a shared sorting strategy. When a [collaborator](/documentation/resource-reference/collaborators) qualifies for multiple programs within the same group, the sorter determines which program takes priority. This lets you set up layered incentive structures. For example, a seasonal bonus program can override the default commission without worrying about double-counting. Each group has a sorter strategy that controls how priority is resolved. The built-in strategies are `oldestBindingWins` (the program with the earliest engagement takes priority) and `newestBindingWins` (the most recently engaged program wins). The relationship between programs and groups is managed through a separate junction datastore in PHP and via extended fields in REST. Program groups do not carry a status field, so there is no soft-delete lifecycle. Deletes are always permanent. ## The program group object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Name | `name` | `getName()` | string | Display name of the group | | Description | `description` | `getDescription()` | string | Human-readable description of the group's purpose | | Sorter | `sorter` | `getSorter()` | string | Sorting strategy identifier (see below) | ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested. | Field | Type | Description | |---|---|---| | `programIds` | integer[] | Array of program IDs assigned to this group | | `programs` | object[] | Array of program objects with `id`, `name`, and `status` for each program in the group | ## Sorting strategies The `sorter` field controls how the system resolves priority when a collaborator matches multiple programs in the same group. | Strategy | Behavior | |---|---| | `oldestBindingWins` | The program the collaborator was bound to first takes priority | | `newestBindingWins` | The most recently bound program takes priority | ## Accessing program group data ```bash # List all program groups curl -X GET "https://your-site.com/wp-json/siren/v1/program-groups?fields=id,name,sorter,programIds" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single program group with full program details curl -X GET "https://your-site.com/wp-json/siren/v1/program-groups/5?fields=id,name,description,sorter,programs" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\ProgramGroups\Core\Datastores\ProgramGroup\Interfaces\ProgramGroupDatastore; class GroupManager { protected ProgramGroupDatastore $groups; public function __construct(ProgramGroupDatastore $groups) { $this->groups = $groups; } public function getGroup(int $groupId): \Siren\ProgramGroups\Core\Models\ProgramGroup { return $this->groups->getById($groupId); } } ``` ```php use Siren\ProgramGroups\Core\Facades\ProgramGroups; $group = ProgramGroups::getById(5); $allGroups = ProgramGroups::andWhere([]); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Program groups are normally configured through the admin UI. If you need to create them programmatically (e.g., during a migration or recipe import), the datastores documented here are the right tool. The group's sorting strategy is applied automatically during the attribution pipeline. You don't need to invoke it manually. ## PHP domain methods ### The program-group junction The relationship between programs and groups is tracked through a junction datastore. Each junction record links one program to one group. A program can belong to at most one group. The junction model has two fields: | Field | PHP getter | Type | Description | |---|---|---|---| | Program ID | `getProgramId()` | integer | The program's primary key (also serves as the junction's identity) | | Group ID | `getGroupId()` | integer | The group this program belongs to | ```php use Siren\ProgramGroups\Core\Datastores\ProgramsProgramGroups\Interfaces\ProgramsProgramGroupsDatastore; class GroupInspector { protected ProgramsProgramGroupsDatastore $junctions; public function __construct(ProgramsProgramGroupsDatastore $junctions) { $this->junctions = $junctions; } public function getProgramsInGroup(int $groupId): array { return $this->junctions->getProgramsForGroup($groupId); } public function findGroupForProgram(int $programId): ?\Siren\ProgramGroups\Core\Models\ProgramGroup { return $this->junctions->getProgramGroupForProgram($programId); } } ``` ```php use Siren\ProgramGroups\Core\Facades\ProgramsProgramGroups; // Get all programs in a group $programs = ProgramsProgramGroups::getProgramsForGroup($groupId); // Find which group a program belongs to (returns null if ungrouped) $group = ProgramsProgramGroups::getProgramGroupForProgram($programId); ``` ### Looking up programs in a group `getProgramsForGroup` retrieves all program models that belong to a specific group. This crosses the junction boundary. It accepts a group ID but returns full `Program` model instances, not junction records. ```php use Siren\ProgramGroups\Core\Facades\ProgramsProgramGroups; $programs = ProgramsProgramGroups::getProgramsForGroup($groupId); foreach ($programs as $program) { echo $program->getName() . ' (' . $program->getIncentiveType() . ')'; } ``` ### Finding a program's group `getProgramGroupForProgram` looks up the group a program belongs to. Returns `null` if the program is not in any group. This is useful for checking whether attribution deduplication applies to a given program. ```php use Siren\ProgramGroups\Core\Facades\ProgramsProgramGroups; $group = ProgramsProgramGroups::getProgramGroupForProgram($programId); if ($group !== null) { echo $program->getName() . ' is in group: ' . $group->getName(); echo 'Priority strategy: ' . $group->getSorter(); } else { echo 'This program is not in a group — no deduplication applies.'; } ``` ## Relationships - **[Programs](/documentation/resource-reference/programs).** A program group contains zero or more programs via a junction table. Each program can belong to at most one group (the `programId` is the primary key on the junction table). The `programs` and `programIds` extended fields expose these associations on read endpoints. - **Sorting and Attribution.** When a collaborator qualifies for multiple programs within the same group, the group's `sorter` strategy determines which program takes precedence. This affects which incentive structure applies to a given [conversion](/documentation/resource-reference/conversions). ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## ProgramBoundToCollaboratorGroup Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group Fires when a program is wired to a collaborator group via the program edit endpoint. # ProgramBoundToCollaboratorGroup `ProgramBoundToCollaboratorGroup` fires when an operator wires a program to a [collaborator group](/documentation/general/what-are-collaborator-groups). The binding is what tells the [cascade](/documentation/general/what-is-a-cascade) machinery which group to walk when a triggering event lands against the program. Without it, a cascade calc strategy has no group to traverse. The event ID is `program_bound_to_collaborator_group`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\ProgramBoundToCollaboratorGroup`. It fires after the binding is saved, from the program edit endpoint. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the program id and the group id that was just bound to it. A program can be bound to at most one collaborator group at a time, so this event names the single binding now in effect. It is the program twin of [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group): the same payload shape and one-group rule, and the same config pattern, differing only by `type=program` in place of `type=distributor`. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\ProgramBoundToCollaboratorGroup; class HandleProgramBinding implements CanHandle { public function handle(Event $event): void { if (!$event instanceof ProgramBoundToCollaboratorGroup) { return; } $programId = $event->getProgramId(); $groupId = $event->getGroupId(); // The next trigger fired against this program will walk this group } } ``` ## How does it fit? The binding is a config row keyed `(type='program', subtype=, configKey='collaboratorGroupId')`. The activity feed listens here to record the new relationship. Any reporting layer that wants to show "which programs are wired to this group" can rebuild its lookup off the same signal. Re-binding a program to a different group fires this event again with the new group id, after firing [ProgramUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group) for the previous one. Both fire synchronously in the same request, so the unbind always precedes the bind. If a trigger lands against a program with no bound group, the cascade has no group to walk and credits no one, with no error raised. If the bound group's structure cannot support the selected calc (a cascade calc on a flat group, for example), the calc [fails closed](/documentation/calculation-strategies/cascade-troubleshooting) at run time and credits no one, so a bind-time handler is a good place to catch an incompatible pairing early. ## Related events [ProgramUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group) is the unbind side, and [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group) and [DistributorUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group) are the distributor-side pair. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## ProgramGroupConversionTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-conversions/program-group-conversion-triggered Fires when a conversion involves programs in a program group. Enforces mutual exclusivity so only one program awards credit. # ProgramGroupConversionTriggered Program groups enforce mutual exclusivity: when a customer is eligible for conversions under multiple programs in the same group, only one program can award credit. When the conversion pipeline detects this situation, it fires `ProgramGroupConversionTriggered` to announce which program won and which programs lost. The event is identified as `program_group_conversion_triggered` and lives in the `Siren\Conversions\Core\Events` namespace. ## What does this event carry? The event provides the winning `Program`, an array of losing `Program` models, the `opportunityId`, and the `Transaction`. ```php use Siren\Conversions\Core\Events\ProgramGroupConversionTriggered; public function handle(Event $event): void { $winner = $event->getWinningProgram(); $losers = $event->getLosingPrograms(); $opportunityId = $event->getOpportunityId(); $transaction = $event->getTransaction(); } ``` ## How is the winner determined? The program group's resolution strategy decides which program wins. The two strategies are newest engagement wins and oldest engagement wins. If a customer clicked affiliate links for two programs in the same group, the resolution strategy looks at when each engagement was created and picks the winner accordingly. ## What happens to the losing programs? Listeners use this event to clean up engagements on the losing programs. Because mutual exclusivity is enforced at the group level, the losing programs' engagements for this opportunity are invalidated so they cannot produce conversions. Only the winning program proceeds through the rest of the conversion flow. This cleanup is important for accurate reporting. Without it, engagements on losing programs would remain active and could appear in dashboards as unconverted activity, misrepresenting the collaborator's performance. See the [Conversion Events overview](/documentation/developer-reference/events-conversions) for how this event fits into the full conversion pipeline. ## Programs Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs Affiliate programs — data model, status lifecycle, REST API, PHP data access, engagement types, and transaction compilers. import CodeTabs from "@/components/content/CodeTabs.astro"; # Programs A program defines a set of rules for how [collaborators](/documentation/resource-reference/collaborators) earn rewards. Each program specifies an incentive structure (how rewards are calculated), engagement types (what actions trigger attribution), and a currency unit (what the rewards are denominated in). Programs are the central organizing concept in Siren. Collaborators are enrolled in programs, and [conversions](/documentation/resource-reference/conversions), [obligations](/documentation/resource-reference/obligations), and [fulfillments](/documentation/resource-reference/fulfillments) all trace back to the program that governs them. ## The program object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Name | `name` | `getName()` | string | Display name of the program | | Description | `description` | `getDescription()` | string | Human-readable description of what the program does | | Incentive type | `incentiveType` | `getIncentiveType()` | string | Incentive type identifier (e.g., `commission`) | | Incentive resolver | `incentiveResolverType` | `getIncentiveResolverType()` | string | Strategy used to resolve incentive amounts | | Status | `status` | `getStatus()` | string | Current status: `active`, `inactive`, `draft`, or `deleted` | | Units | `units` | `getUnits()` | string | Currency or unit identifier for reward denominations (e.g., `USD`, `points`) | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the record was created | | Modified | `dateModified` | `getModifiedDate()` | datetime | When the record was last updated | ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested: | Field | Type | Description | |---|---|---| | `collaborators` | array | List of collaborators enrolled in this program, each with `id` and `nickname` | | `collaboratorCount` | integer | Total number of collaborators enrolled in this program | | `engagementsCount` | integer | Total number of engagements associated with this program | | `engagementTypes` | array | Engagement type configurations for this program, each with `id`, `type`, and `value` | | `transactionCompilers` | array | Active transaction compilers for this program, each with `id` and `compiler` | | `incentiveConfig` | object | Combined incentive type and resolver metadata, each with `id` and `name` | | `incentiveCalculation` | object | Incentive calculation settings including `transactionPercent`, `payoutPerTransaction`, `payoutPerProduct`, `payoutPerLead`, `activeConversionTypes`, `autoApprove`, and `leadRequalificationDays` | | `expirationDays` | integer or null | Number of days before the program's conversions expire, or null if no expiration is set | | `lineItemFilters` | object or null | Transaction line item filtering rules including `withTypes`, `collaboratorOwned`, `withSkus`, and `inCategories` | When extended fields are requested, `incentiveType` and `incentiveResolverType` are enriched from plain string identifiers to objects containing both `id` and `name`. ## Status lifecycle | Status | Description | |---|---| | `active` | Program is live and accepting new engagements and conversions. | | `inactive` | Program is paused. Existing data is preserved, but new activity is not processed. | | `deleted` | Soft-deleted. A second DELETE call permanently removes the record. Bulk restore moves the program back to inactive. | ## Accessing program data ```bash # List active programs curl -X GET "https://your-site.com/wp-json/siren/v1/programs?status=active" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single program with extended fields curl -X GET "https://your-site.com/wp-json/siren/v1/programs/5?fields=id,name,status,collaboratorCount,engagementTypes,incentiveCalculation" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore; class MyService { protected ProgramDatastore $programs; public function __construct(ProgramDatastore $programs) { $this->programs = $programs; } public function getActivePrograms(): array { return $this->programs->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); } } ``` ```php use Siren\Programs\Core\Facades\Programs; $activePrograms = Programs::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); $program = Programs::getById(42); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). If you find yourself creating programs manually in code, consider whether your use case is better served by the admin UI or the recipe import system. Direct datastore writes bypass the normal event flow and can leave related records out of sync. ## PHP domain methods ### Looking up a collaborator's programs `getCollaboratorPrograms` retrieves all programs a specific collaborator participates in. This queries the junction table that links collaborators to programs and returns the full program models. ```php use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore; class CollaboratorDashboard { protected ProgramDatastore $programs; public function __construct(ProgramDatastore $programs) { $this->programs = $programs; } public function listPrograms(int $collaboratorId): array { // Returns Program[] - defaults to 10 results starting at offset 0 return $this->programs->getCollaboratorPrograms($collaboratorId); } public function listAllPrograms(int $collaboratorId): array { // Paginate with custom limit and offset return $this->programs->getCollaboratorPrograms($collaboratorId, 50, 0); } } ``` ```php use Siren\Programs\Core\Facades\Programs; // Get first 10 programs for a collaborator $programs = Programs::getCollaboratorPrograms($collaboratorId); // With pagination $programs = Programs::getCollaboratorPrograms($collaboratorId, 25, 0); ``` ### Counting collaborators in a program `getCollaboratorCount` returns the estimated number of collaborators enrolled in a given program. ```php $count = $this->programs->getCollaboratorCount($programId); ``` ```php use Siren\Programs\Core\Facades\Programs; $count = Programs::getCollaboratorCount($programId); ``` ## Engagement types Each program can be configured with one or more engagement types that define which kinds of engagements are tracked and how they are valued. Engagement types are stored as separate records linked to a program by `programId`. The engagement type model (`Siren\Programs\Core\Models\ProgramEngagementType`) has the following fields: | Field | PHP getter | Type | Description | |---|---|---|---| | ID | `getId()` | integer | Primary key | | Program | `getProgramId()` | integer | The program this engagement type belongs to | | Engagement type | `getEngagementType()` | string | Type identifier (e.g., `saleTransaction`, `leadCapture`) | | Engagement value | `getEngagementValue()` | float | Numeric value or weight assigned to this engagement type | ### Accessing engagement type data ```php use Siren\Programs\Core\Datastores\ProgramEngagementTypes\Interfaces\ProgramEngagementTypeDatastore; class IncentiveCalculator { protected ProgramEngagementTypeDatastore $engagementTypes; public function __construct(ProgramEngagementTypeDatastore $engagementTypes) { $this->engagementTypes = $engagementTypes; } public function getAllEngagementTypes(int $programId): array { return $this->engagementTypes->getProgramEngagementTypes($programId); } public function getSaleValue(int $programId): float { $type = $this->engagementTypes->getEngagementTypeForProgram( $programId, 'saleTransaction' ); return $type->getEngagementValue(); } } ``` ```php use Siren\Programs\Core\Facades\ProgramEngagementTypes; // Get all engagement types for a program $types = ProgramEngagementTypes::getProgramEngagementTypes($programId); // Get a specific engagement type configuration // Throws RecordNotFoundException if the program doesn't have this type configured $saleType = ProgramEngagementTypes::getEngagementTypeForProgram( $programId, 'saleTransaction' ); $value = $saleType->getEngagementValue(); ``` `getProgramEngagementTypes` returns all engagement types configured for a program. `getEngagementTypeForProgram` retrieves a single engagement type by program ID and type identifier, and throws `RecordNotFoundException` if the combination doesn't exist. ## Transaction compilers Transaction compilers define how a program calculates transaction amounts. Each program can have one or more compilers registered, and each compiler record links a program to a compiler strategy identifier. For a user-level overview of the built-in filters (line items, discounts, fees, shipping, taxes) and how they're configured through the admin UI, see [Line Item Filters](/documentation/general/line-item-filters). The transaction compiler model (`Siren\Programs\Core\Models\ProgramTransactionCompiler`) has the following fields: | Field | PHP getter | Type | Description | |---|---|---|---| | ID | `getId()` | integer | Primary key | | Program | `getProgramId()` | integer | The program this compiler belongs to | | Compiler | `getTransactionCompiler()` | string | The compiler strategy identifier | ### Accessing transaction compiler data ```php use Siren\Programs\Core\Datastores\ProgramTransactionCompilers\Interfaces\ProgramTransactionCompilerDatastore; class CompilerInspector { protected ProgramTransactionCompilerDatastore $compilers; public function __construct(ProgramTransactionCompilerDatastore $compilers) { $this->compilers = $compilers; } public function getCompilers(int $programId): array { return $this->compilers->getProgramTransactionCompilers($programId); } } ``` ```php use Siren\Programs\Core\Facades\ProgramTransactionCompilers; $compilers = ProgramTransactionCompilers::getProgramTransactionCompilers($programId); foreach ($compilers as $compiler) { echo $compiler->getTransactionCompiler(); } ``` `getProgramTransactionCompilers` returns all transaction compiler records for a given program. ## Relationships - **[Collaborators](/documentation/resource-reference/collaborators)** (enrollment). Collaborators are enrolled in programs. The `collaborators` and `collaboratorCount` extended fields surface this relationship. A collaborator may belong to multiple programs. - **Engagement Types** (configuration). Each program defines which [engagement](/documentation/resource-reference/engagements) types are valid for attribution. These are stored in a junction table and surfaced via the `engagementTypes` extended field. - **Transaction Compilers** (configuration). Programs declare which [transaction](/documentation/resource-reference/transactions) compilers apply when calculating conversion value. Surfaced via the `transactionCompilers` extended field. - **[Program Groups](/documentation/resource-reference/program-groups)** (organization). Programs can be organized into groups via a junction table. The list endpoint supports filtering by `programGroupId`. - **[Conversions](/documentation/resource-reference/conversions)** (downstream). When an engagement attributed to a program results in a tracked outcome, a conversion is created referencing the program. - **[Obligations](/documentation/resource-reference/obligations)** (downstream). Approved conversions produce obligations governed by the program's incentive configuration. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## ProgramUnboundFromCollaboratorGroup Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-collaborator-groups/program-unbound-from-collaborator-group Fires when a program's collaborator group binding is cleared. # ProgramUnboundFromCollaboratorGroup `ProgramUnboundFromCollaboratorGroup` fires when an operator clears a program's [collaborator group](/documentation/general/what-are-collaborator-groups) binding. After this event, the program no longer has a group for [cascade](/documentation/general/what-is-a-cascade) strategies to walk. Any cascade calc still configured on the program [fails closed](/documentation/calculation-strategies/cascade-troubleshooting): it credits no one and raises no error, and it keeps doing so on every trigger until the program is bound to a group again. That silent zero-payout is the main reason to handle this event. The event ID is `program_unbound_from_collaborator_group`, and its fully qualified class is `Siren\Plus\Core\Groups\Events\ProgramUnboundFromCollaboratorGroup`. It fires after the binding is cleared, from the program edit endpoint. To run code when it fires, register a handler with Siren's event system. See [listeners](/documentation/extensions/listeners) and the [events introduction](/documentation/developer-reference/events-introduction). ## What does this event carry? The event carries the program id and the group id that the program was previously bound to. That historical group id is included specifically so that audit listeners and external mirrors can describe what was removed, not just that something was removed. ```php use PHPNomad\Events\Interfaces\CanHandle; use PHPNomad\Events\Interfaces\Event; use Siren\Plus\Core\Groups\Events\ProgramUnboundFromCollaboratorGroup; class HandleProgramUnbinding implements CanHandle { public function handle(Event $event): void { if (!$event instanceof ProgramUnboundFromCollaboratorGroup) { return; } $programId = $event->getProgramId(); $previousGroupId = $event->getGroupId(); // Cascade calcs on this program now have no group to walk } } ``` ## How does it fit? The unbind event is only broadcast when there was a real prior binding to clear. Clearing an already-unbound program is a silent no-op so the activity feed doesn't fill up with phantom events from idempotent edits. Re-binding to a different group produces an unbind for the old group id immediately followed by a [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group) for the new one. Both fire synchronously in the same request, so the unbind precedes the bind. This event tracks the deliberate unbind or rebind action, not the liveness of the binding. Deleting the bound group [does not fire it](/documentation/developer-reference/events-collaborator-groups/collaborator-group-deleted): the binding row stays on the program, now pointing at a group that is gone, and a cascade bound to it fails closed. Deleting the program does not fire it either. So a system that mirrors which programs point at which groups must reconcile group and program deletions on its own rather than rely on this event alone. ## Related events [ProgramBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/program-bound-to-collaborator-group) is the bind side, and [DistributorBoundToCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-bound-to-collaborator-group) and [DistributorUnboundFromCollaboratorGroup](/documentation/developer-reference/events-collaborator-groups/distributor-unbound-from-collaborator-group) are the distributor-side pair. The [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) lists the full family. ## Querying Data Source: https://www.sirenaffiliates.com/documentation/extensions/wp-querying-data How get_post, WP_Query, and $wpdb translate to Siren's typed datastore system. import CodeTabs from "@/components/content/CodeTabs.astro"; # Querying Data In WordPress, you query data through `get_post()`, `WP_Query`, `get_posts()`, or direct `$wpdb` calls. Siren replaces all of these with typed [datastores](https://phpnomad.com/core-concepts/datastores/introduction) (one per domain model) that return typed model instances with getter methods. No more working with generic `WP_Post` objects or raw database rows. ## Fetching a single record The most common operation: get one record by ID. In WordPress this is `get_post()`. In Siren, each domain model has a facade and a datastore with `getById()`. ```php // WordPress: returns a generic WP_Post object $post = get_post(42); echo $post->post_title; echo get_post_meta(42, '_custom_field', true); ``` ```php // Siren facade: returns a typed Program model use Siren\Programs\Core\Facades\Programs; $program = Programs::getById(42); echo $program->getName(); echo $program->getStatus(); // IDE autocomplete shows all available getters ``` ```php // Siren DI: inject the datastore, call getById() use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore; class MyService { protected ProgramDatastore $programs; public function __construct(ProgramDatastore $programs) { $this->programs = $programs; } public function getProgramName(int $id): string { $program = $this->programs->getById($id); return $program->getName(); } } ``` The model you get back is a typed object, not a generic post or array. `$program->getName()` returns a string. `$program->getStatus()` returns a status value. Your IDE knows every available method. ## Filtered queries WordPress provides `get_posts()` with meta queries and `$wpdb->get_results()` for direct SQL. Siren uses `andWhere()` and `where()` with a structured condition array. ```php // WordPress: WP_Query with meta query $posts = get_posts([ 'post_type' => 'program', 'meta_key' => 'status', 'meta_value' => 'active', ]); // Or direct $wpdb for complex queries global $wpdb; $results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}programs WHERE status = 'active'" ); ``` ```php // Siren facade: andWhere() with typed conditions use Siren\Programs\Core\Facades\Programs; $activePrograms = Programs::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'], ]); // Returns an array of typed Program model instances ``` ```php // Siren DI: same query through the injected datastore use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore; class ProgramListService { protected ProgramDatastore $programs; public function __construct(ProgramDatastore $programs) { $this->programs = $programs; } public function getActivePrograms(): array { return $this->programs->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'], ]); } public function getActiveProgramsPaginated(int $page, int $perPage): array { return $this->programs->where( [['column' => 'status', 'operator' => '=', 'value' => 'active']], $perPage, // limit ($page - 1) * $perPage, // offset 'name', // order by 'asc' // direction ); } } ``` ## Supported query operators The condition array supports the following operators: | Operator | Example | |---|---| | `=` | `['column' => 'status', 'operator' => '=', 'value' => 'active']` | | `!=` | `['column' => 'status', 'operator' => '!=', 'value' => 'draft']` | | `>` | `['column' => 'total', 'operator' => '>', 'value' => 1000]` | | `<` | `['column' => 'total', 'operator' => '<', 'value' => 5000]` | | `>=` | `['column' => 'quantity', 'operator' => '>=', 'value' => 1]` | | `<=` | `['column' => 'quantity', 'operator' => '<=', 'value' => 100]` | | `LIKE` | `['column' => 'name', 'operator' => 'LIKE', 'value' => '%affiliate%']` | | `NOT LIKE` | `['column' => 'name', 'operator' => 'NOT LIKE', 'value' => '%test%']` | | `IN` | `['column' => 'status', 'operator' => 'IN', 'value' => ['active', 'pending']]` | | `NOT IN` | `['column' => 'status', 'operator' => 'NOT IN', 'value' => ['archived']]` | Multiple conditions are combined with AND. Each condition is an associative array with `column`, `operator`, and `value` keys. ## What is different from WordPress queries? Every query returns typed model instances, not generic arrays or `WP_Post` objects. You call `$program->getName()` instead of `$post->post_title`. The column names are real database column names, not meta keys. There is no meta table indirection. Datastores operate on Siren's own database tables, not the WordPress posts/postmeta tables. Siren does not use custom post types for its domain models. Programs, collaborators, conversions, and other entities each have dedicated tables with proper columns and indexes. For the full query syntax including pagination, sorting, and all available datastore methods, see the [Resource Reference Introduction](/documentation/resource-reference/introduction). For the full PHPNomad framework documentation on datastores, see [Datastores Introduction](https://phpnomad.com/core-concepts/datastores/introduction). ## Quick Reference Source: https://www.sirenaffiliates.com/documentation/extensions/wp-quick-reference WordPress-to-Siren pattern mapping table with links to detailed guides. # WordPress Developer Quick Reference Siren is built on [PHPNomad](https://phpnomad.com/), a platform-agnostic PHP framework. None of Siren's core logic depends on WordPress. It uses WordPress as a host platform through a thin integration layer. This means the patterns you use daily in WordPress plugin development have direct equivalents in Siren, but they are expressed through typed PHP interfaces rather than global functions and string-named hooks. This guide maps WordPress patterns you already know to their Siren equivalents. Each row links to a dedicated page with side-by-side code examples. The rest of Siren's documentation is platform-agnostic by design. It describes PHPNomad interfaces, domain events, and datastores without reference to WordPress. These WordPress Developer Guide pages are the bridge: they start from the WordPress concept and show you where to land. If you are building a Siren extension for a WordPress plugin, start here and follow the links into the detailed guides. If you have already read through the [Getting Started](/documentation/extensions/architecture-overview) section, use this table as a quick-lookup cheat sheet. ## Pattern Mapping | WordPress Pattern | Siren Equivalent | Details | |---|---|---| | `add_action` / `add_filter` | `Event::attach()` or `CanHandle` + `HasListeners` | [Hooks, Actions & Events](/documentation/extensions/wp-hooks-and-events) | | `do_action` / `apply_filters` | `Event::broadcast()` or `EventStrategy::broadcast()` | [Hooks, Actions & Events](/documentation/extensions/wp-hooks-and-events) | | WordPress hook to domain event | Event bindings + transformer callables | [Bridging Platform Hooks](/documentation/extensions/wp-bridging-hooks) | | `get_post` / `WP_Query` | `Facade::getById()` / `andWhere()` or datastore DI | [Querying Data](/documentation/extensions/wp-querying-data) | | `$wpdb` direct queries | Datastore `andWhere()` / `where()` | [Querying Data](/documentation/extensions/wp-querying-data) | | Global functions | Facades (static wrappers) | [Dependency Injection](/documentation/extensions/wp-dependency-injection) | | `get_option` / `update_option` | `Configs::getConfigValue()` or `ConfigDatastore` DI | [Configuration](/documentation/extensions/wp-configuration) | | `get_post_meta` / `update_post_meta` | `Configs::setConfig()` (scoped by type/subtype) | [Configuration](/documentation/extensions/wp-configuration) | | Custom post types | Model classes + datastores | [Architecture Overview](/documentation/extensions/architecture-overview) | | `register_rest_route` | `Controller` + `HasMiddleware` + `HasControllers` | [REST API Patterns](/documentation/extensions/wp-rest-api) | | `current_user_can` | `User::canDoAction()` or auth middleware | [Roles & Permissions](/documentation/extensions/wp-roles-permissions) | | WP-Cron / `wp_schedule_event` | Domain event listeners or `TaskStrategy::dispatch()` | [PHPNomad Tasks](https://phpnomad.com/core-concepts/tasks/) | | `WP_Error` / `is_wp_error()` | Typed exceptions (`RecordNotFoundException`, etc.) | [Error Handling](/documentation/extensions/wp-error-handling) | | Plugin activation hooks | Initializers + `shouldLoad()` | [The Integration Class](/documentation/extensions/integration-class) | ## Quick Start: Your First Program Source: https://www.sirenaffiliates.com/documentation/getting-started/quick-start Set up your first affiliate program in 10 minutes. The bare minimum to get a working program with Siren installed. import StepList from "@/components/content/StepList.astro"; You just installed Siren and you want a working program right now, not a tour of every option. This page is for you. Start by picking the shape of your program, then pick how you want to install it. ## What are you building? Siren handles a lot more than traditional affiliate programs, so the first step is figuring out which shape your program takes. Pick the closest match below and you'll land on a recipe that's already configured for that use case. You can tune it afterward. - [Affiliate program](/recipes/basic-affiliate-program) is the standard "pay a commission when a referral leads to a sale" setup. If this is your first program and you're not sure which one fits, start here. - [LMS revenue share](/recipes/instructor-revenue-share) pays instructors based on sales of the courses they teach. Good for course platforms where instructors bring their own audiences. - [Marketplace vendor royalties](/recipes/marketplace-vendor-commission) pays vendors when their products sell. Fits Etsy-style storefronts where you host products created by other people. - [SaaS partner program](/recipes/business-partner-revenue-share) pays recurring commissions to integration partners, agencies, and resellers. Built around renewals rather than one-time sales. - [Sales team SPIF or internal bonus](/recipes/sales-team-commission-program) pays your own salespeople a per-sale bonus. Same tracking model as an affiliate program, but the collaborators are employees. - [Tiered sales override](/recipes/tiered-sales-override-program) pays a collaborator's upline when their referral converts, crediting each layer of a sales hierarchy. This is the override pattern used by organizations and sales teams, where a manager earns on the production of the team beneath them. It is built on a [collaborator group](/documentation/general/what-are-collaborator-groups) and a [cascade](/documentation/general/what-is-a-cascade). Requires Siren Pro. If none of these match what you're building, browse the full [recipe library](/recipes) or skip to the Beacon path below and describe your program in plain English. ## How do I install one? Once you've picked a shape, you've got three ways to actually get it running. All three land you in the same place: a live program you can enroll collaborators in. ### The fastest path: install a recipe Recipes are pre-built, fully-configured programs you can drop into your site with one click. They're the shortest distance between "Siren is installed" and "I have a working program," because all the decisions about commission structure, attribution, and tracking events have already been made for you. You can always tweak things afterward. Whichever recipe you picked above becomes the starting point here. If you're still browsing, the [Basic Affiliate Program](/recipes/basic-affiliate-program) recipe is a solid default for a standard percentage-based setup. That's the whole flow. Next, you'll want to [add a collaborator](/documentation/getting-started/managing-collaborators-affiliates) so someone can actually start earning, and then [test the flow with a manual transaction](/documentation/getting-started/creating-transactions-manually) to confirm everything's wired up. ### The guided path: build with Beacon If you want more control than a recipe gives you but you don't want to learn the program edit screen first, [Beacon](/documentation/getting-started/what-is-beacon) is Siren's free AI assistant. Describe what you want in plain English ("a 30% commission program for my course creators, with a 14-day attribution window") and Beacon will design the program for you and generate a recipe you can install with one click. Beacon is the right choice when your idea doesn't quite match an existing recipe but you don't want to read the full tutorial either. It's free and doesn't require a Siren license. ### The manual path: build it yourself If you'd rather learn how Siren works as you go, you can build your first program by hand from the WordPress admin. The full walkthrough is in [Create a Program in Siren](/documentation/getting-started/create-a-program-in-siren). It covers every option on the program edit screen, so plan on spending more like 20 to 30 minutes the first time through. ## What you'll need before going live Whichever path you pick, a working program needs three things in place before real money moves through it. Collaborators, or [open registration](/documentation/getting-started/set-up-a-program-registration-form) so people can sign themselves up. Full walkthrough is in [managing collaborators](/documentation/getting-started/managing-collaborators-affiliates).", }, { title: "A way to test the flow", description: "Before you announce the program to the world, [create a manual transaction](/documentation/getting-started/creating-transactions-manually) and confirm the conversion and obligation show up the way you expect. This catches configuration mistakes before real money is involved.", }, ]} /> ## Where to go next Once your program is live, these are the docs you'll reach for next: - [Managing collaborators](/documentation/getting-started/managing-collaborators-affiliates) for adding affiliates and sending login emails - [How to pay collaborators](/documentation/getting-started/how-to-pay-collaborators) for the payout side of things - [Creating transactions manually](/documentation/getting-started/creating-transactions-manually) for testing and one-off attribution - [What is an affiliate program?](/documentation/general/what-is-an-affiliate-program) if you want background on how the pieces fit together - [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group) if you need a roster that pays a collaborator's upline or downline (tiered or sales-override structures) - [How to find your first affiliates](/blog/how-to-find-your-first-affiliates) for where to actually source the people you're about to enroll - [How much should you pay your affiliates](/blog/how-much-should-you-pay-affiliates) if you're still working out the commission rate ## Quickstart: Your First Extension Source: https://www.sirenaffiliates.com/documentation/extensions/quickstart Step-by-step walkthrough to scaffold, configure, and activate your first Siren extension. # Quickstart: Your First Extension This tutorial walks you through building a Siren extension from the template repository. Siren's extension system is built on [PHPNomad](https://phpnomad.com/), a platform-agnostic PHP framework. By the end, you will have a working extension that listens to a WordPress hook and triggers a Siren domain event. ## What You Will Build A simple integration that fires a `SaleTriggered` domain event when your custom plugin processes an order. This is the most common extension pattern. It bridges a third-party commerce system into Siren's affiliate tracking. ## Prerequisites - A WordPress development environment with Siren installed and activated - Composer installed globally - Basic familiarity with PHP 8.1+ and WordPress plugin development - Understanding of Siren's [Extension Architecture](/documentation/extensions/architecture-overview) ## Step 1: Clone the Template ```bash cd wp-content/plugins/ git clone https://github.com/Novatorius/siren-extension-template.git siren-my-plugin cd siren-my-plugin rm -rf .git git init ``` You now have a plugin directory with this structure: ``` siren-my-plugin/ plugin.php composer.json lib/ Integration.php Transformers/ SaleTriggeredTransformer.php Adapters/ OrderToTransactionDetailsAdapter.php Handlers/ AdminHandler.php ``` ## Step 2: Find-and-Replace Naming Placeholders The template uses placeholder names that you need to replace with your own. Do a project-wide find-and-replace for each of these, in this order: | Find | Replace With | Where It Appears | |------|-------------|------------------| | `YourExtension` | `MyPlugin` | Namespace segments, class references | | `Your Extension` | `My Plugin` | Plugin header, human-readable names | | `your-extension` | `my-plugin` | Plugin slug, directory references | | `your_extension` | `my_plugin` | Constants, function prefixes | | `your_ext` | `my_plugin` | Short identifiers | | `Novatorius\\SirenExtensionTemplate` | `YourVendor\\SirenMyPlugin` | Namespace root in composer.json | Be methodical. Check every file. A missed replacement will cause autoloading failures or namespace collisions. ## Step 3: Update plugin.php Metadata Open `plugin.php` and update the WordPress plugin header: ```php new Integration()); }, 0); ``` The early return on `add_action` prevents the file from executing outside WordPress. This guard is required. Extensions must register at priority `0` on `siren_ready` so all extensions are registered before Siren processes them. The callable passed to `Extensions::add()` is a lazy factory. Your `Integration` class is not instantiated until Siren is ready to process it, keeping registration cheap. ## Step 4: Configure composer.json Update the namespace mapping so Composer's autoloader can find your classes: ```json { "name": "your-vendor/siren-my-plugin", "autoload": { "psr-4": { "YourVendor\\SirenMyPlugin\\": "lib/" } }, "require": { "php": ">=8.1" } } ``` Then install: ```bash composer install ``` This generates the `vendor/autoload.php` that your `plugin.php` should require. If the template does not include a `require` statement for the autoloader, add one before the `siren_ready` hook: ```php require_once __DIR__ . '/vendor/autoload.php'; ``` ## Step 5: Set up activation and load conditions Open `lib/Integration.php`. The two gating methods control whether your extension loads: ```php public function canActivate(): bool { // Return true if the third-party plugin your extension // integrates with is installed and available. return class_exists('MyPluginMainClass'); } public function shouldLoad(): bool { // For most integrations, this mirrors canActivate(). // Use a different condition if you need finer control // (e.g., checking a settings flag). return $this->canActivate(); } ``` `canActivate()` is used by Siren's admin UI to show whether the extension *could* work. Are the dependencies present? `shouldLoad()` is checked by the loader to decide whether to actually process the extension right now. Usually it delegates to `canActivate()`, but you might add additional checks like verifying that a required API key is configured. If `shouldLoad()` returns `false`, the entire extension is skipped: no event bindings, no listeners, no `load()` call. ## Step 6: Declare What Your Extension Supports The `getSupports()` method tells Siren what capabilities your extension provides. This affects which features are available in the admin UI: ```php use Siren\Extensions\Core\Enums\Features; public function getSupports(): array { return [ Features::ManualOrdering, // Add Features::Coupons if your plugin has coupon support // Add Features::Renewals if your plugin has subscription support ]; } ``` Available feature constants: | Constant | Meaning | |----------|---------| | `Features::Coupons` | Extension can apply affiliate coupons | | `Features::Courses` | Extension tracks course enrollments | | `Features::Lessons` | Extension tracks lesson completions | | `Features::Posts` | Extension tracks post interactions | | `Features::Renewals` | Extension tracks subscription renewals | | `Features::Forms` | Extension tracks form submissions | | `Features::ManualOrdering` | Extension supports manual order creation | ## Step 7: Add Your First Event Binding Event bindings are the core of a third-party integration. They map WordPress hooks from the integrated plugin to Siren domain events. In `lib/Integration.php`, implement `getEventBindings()`: ```php public function getEventBindings(): array { $saleTransformerCallback = fn($orderId) => $this->container->get( SaleTriggeredTransformer::class )->getSaleTriggeredEvent($orderId); return [ SaleTriggered::class => [ [ 'action' => 'my_plugin_order_completed', 'transformer' => $saleTransformerCallback, ], ], ]; } ``` This tells Siren: "When the WordPress action `my_plugin_order_completed` fires, call my transformer. If the transformer returns a `SaleTriggered` event, broadcast it." The binding format is: ```php [ DomainEventClass::class => [ [ 'action' => 'wordpress_hook_name', 'transformer' => callable, ], // You can bind multiple hooks to the same event ], ] ``` The transformer callable receives whatever arguments WordPress passes to the hook. It must return either a domain event instance or `null` (to silently skip). ## Step 8: Create the Transformer The transformer bridges the gap between the WordPress hook's raw arguments and Siren's domain event. Open `lib/Transformers/SaleTriggeredTransformer.php`: ```php opportunityLocator = $opportunityLocator; $this->visitorLocators = $visitorLocators; $this->detailsAdapter = $detailsAdapter; } public function getSaleTriggeredEvent(int $orderId): ?SaleTriggered { // 1. Get the customer's user ID from your plugin's order $userId = my_plugin_get_order_user_id($orderId); // 2. Find the affiliate opportunity for this visitor $opportunity = $this->opportunityLocator->locateUsing( ...$this->visitorLocators->build($userId) ); // No opportunity means no affiliate referred this customer if (!$opportunity) { return null; } // 3. Build the domain event return new SaleTriggered( $opportunity->getId(), $this->detailsAdapter->toArray($orderId), 'my_plugin', // source identifier (string) $orderId, // external binding ID 'my_plugin_order' // external binding type ); } } ``` All three constructor dependencies are resolved automatically by the DI container. If no affiliate opportunity exists for the order, the transformer returns `null` and Siren silently skips the event. The source identifier (`'my_plugin'`) tags the event so Siren knows which integration generated it, and the binding ID and type let Siren deduplicate. If the same order fires the hook twice, Siren can detect the duplicate. ## Step 9: Create the Adapter The adapter converts your plugin's order data into Siren's transaction detail format. Open `lib/Adapters/OrderToTransactionDetailsAdapter.php`: ```php priceAdapter = $priceAdapter; } /** * Convert an order into Siren's transaction details format. * * @param int $orderId * @return array */ public function toArray(int $orderId): array { $order = my_plugin_get_order($orderId); $result = []; foreach ($order->getItems() as $item) { $result[] = [ 'name' => $item->getName(), 'description' => $item->getQuantity() . ' X ' . $item->getName(), 'type' => 'product', 'value' => $this->priceAdapter->toInt($item->getPrice()), 'quantity' => $item->getQuantity(), 'units' => $order->getCurrency(), 'externalId' => $item->getId(), ]; } return $result; } } ``` Siren stores prices as integers (cents, not dollars). Use `FloatToIntPriceAdapter::toInt()` to convert `29.99` to `2999`. The transaction detail array format: | Field | Type | Description | |-------|------|-------------| | `name` | string | Line item display name | | `description` | string | Human-readable description | | `type` | string | `product`, `shipping`, `tax`, `fee`, or `discount` | | `value` | int | Price in smallest currency unit (cents) | | `quantity` | int | Number of units | | `units` | string | Currency code (e.g., `USD`) | | `externalId` | string/null | ID in the source system | ## Step 10: Activate and Test Activate your extension in the WordPress plugin admin — it should appear as a separate plugin. Then go to the Siren admin and verify your extension shows in the extensions list. If `canActivate()` returns `true` and Siren has loaded, the extension should show as active. To test the event flow, create a test affiliate link in Siren, visit your site through that link to create an opportunity, complete an order through your plugin, and check the Siren admin for a new transaction. If something isn't working, start with the basics. If the extension doesn't appear at all, make sure `siren_ready` is firing (Siren must be active). If it appears but shows as inactive, check your `canActivate()` method — the third-party plugin may not be detected. If events aren't firing, verify the WordPress hook name in your event bindings matches exactly what your plugin fires. And if the transformer returns `null`, the event is silently skipped — add logging to diagnose opportunity lookup failures. ## What to Do Next If your plugin has coupon support, implement the coupon admin service pattern so affiliates can use coupon codes — see the WooCommerce and EDD extensions for examples. If your extension needs to react to Siren domain events (not just produce them), implement listeners and create handler classes. You can also use the load method to register admin services that hook into WordPress's admin pages. The [Template Reference](/documentation/extensions/template-reference) explains every file in the template and when to use each pattern. ## RecipeApplyRequested Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-system/recipe-apply-requested Fires when a recipe should be applied to the current installation. A mutable event for program configuration import. # RecipeApplyRequested `RecipeApplyRequested` fires when a recipe should be applied to the current installation. Recipes are fully resolved JSON configurations that describe programs, program groups, and distributors. This event is the mechanism that turns a recipe payload into actual records in the database, and it is mutable: handlers record what they create so the caller can report the results. The event ID is `recipe_apply_requested`, and its fully qualified class is `Siren\Recipes\Core\Events\RecipeApplyRequested`. ## What does this event carry? The event carries a `RecipePayload` model, which contains the deserialized recipe data describing the programs, program groups, and distributors that should be created. ```php use Siren\Recipes\Core\Events\RecipeApplyRequested; public function handle(Event $event): void { $payload = $event->getRecipePayload(); // The payload contains the full recipe configuration: // programs, program groups, and distributors to create } ``` ## Why is this event mutable? Unlike most events in Siren, `RecipeApplyRequested` is designed for handlers to write back to it. As each handler processes its portion of the recipe, it records what it created using the event's mutation methods: `addCreatedProgram`, `addCreatedProgramGroup`, and `addCreatedDistributor`. After all handlers have run, the caller can inspect the event to see exactly what was created, enabling confirmation messages and error reporting. ```php public function handle(Event $event): void { // After creating a program from the recipe payload... $event->addCreatedProgram($program); // After creating a program group... $event->addCreatedProgramGroup($programGroup); // After creating a distributor... $event->addCreatedDistributor($distributor); } ``` This pattern keeps recipe application decoupled. Each domain handles its own portion of the recipe independently, and the event itself serves as the coordination point that tracks the aggregate result. For the full lifecycle of system events and how they connect to the rest of Siren's architecture, see the [System Events overview](/documentation/developer-reference/events-system). ## RefundTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce/refund-triggered Fires when a completed order is reversed through cancellation, refund, or deletion. # RefundTriggered `RefundTriggered` fires when a completed order is reversed. This covers cancellations, refunds, and order deletions. Extensions typically bind this event to multiple platform hooks to catch all the ways an order can be undone. The event ID is `refund_triggered`, and its fully qualified class is `Siren\Commerce\Events\RefundTriggered`. ## What does this event carry? Like `TransactionCompleted`, this event carries the `Transaction` model. The transaction record contains all the context needed to identify the conversions and metrics that need to be reversed. ## How does the pipeline react? Three core listeners react when a refund fires. `CancelConversions` cancels all conversions tied to the transaction. `CancelRefundedTransactions` marks the transaction record itself as refunded. `SubtractRefundedValueFromMetrics` adjusts the incentive and distribution metric totals so that collaborator performance numbers stay accurate. Together, these listeners ensure that a refund cleanly unwinds the entire chain of records that the original sale created. For a user-level walkthrough of how refunds propagate through the system, see [How Refunds Work](/documentation/general/how-refunds-work). For the full lifecycle of commerce events and how they connect to the rest of the attribution pipeline, see the [Commerce Events overview](/documentation/developer-reference/events-commerce). ## Release 1.2.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-1-2-0-4 Adds support for distributions, making it possible to create a whole new category of program structures. Adds support for filtering out the parts of a transaction that’s eligible for rewards. Makes it possible to set a collaborator as the owner of a product. Used for royalty-type programs. New program structure - every engagement wins. This [...] - Adds support for distributions, making it possible to create a whole new category of program structures. - Adds support for filtering out the parts of a transaction that’s eligible for rewards. - Makes it possible to set a collaborator as the owner of a product. Used for royalty-type programs. - New program structure – every engagement wins. This will pay everyone who had at least one engagement with a program. - New tracking event – course completed. This will credit a collaborator when a course owned by them is completed. - New tracking event – lesson completed. This will credit a collaborator when a lesson inside of a course owned by them is completed. - Implements the LifterLMS support ## Release 1.2.1 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-1-2-1 Fixes an issue that caused issue with the post types being incorrectly set on admin screens. - Fixes an issue that caused issue with the post types being incorrectly set on admin screens. ## Release 1.2.2 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-1-2-2 Fixes an issue that caused a fatal when attempting to create a program without transaction filters. - Fixes an issue that caused a fatal when attempting to create a program without transaction filters. ## Release 1.3.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-1-3-0 Updates mapping table to support string identifiers - Updates mapping table to support string identifiers ## Release 1.3.1 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-1-3-1 Resolves error that some users experienced at checkout - Resolves error that some users experienced at checkout ## Release 1.4.0 - EDD Source: https://www.sirenaffiliates.com/documentation/changelogs/release-1-4-0-edd New Integration - Easy Digital Downloads. Siren now fully supports Easy Digital Downloads. Fixes various issues that caused Siren to crash on some WooCommerce installs. - **New Integration** – Easy Digital Downloads. Siren now fully supports Easy Digital Downloads. - Fixes various issues that caused Siren to crash on some WooCommerce installs. ## Release 1.9.25 Source: https://www.sirenaffiliates.com/documentation/changelogs/1925 Development: Adds Aliases facade Development: Improves engagement trigger event to include the strategy ID Fix: Fixes issue that caused some people to be unable to activate their license - Development: Adds Aliases facade - Development: Improves engagement trigger event to include the strategy ID - Fix: Fixes issue that caused some people to be unable to activate their license ## Release 2.0.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-2-0-0 This release is primarily focused on updating the dependencies in Siren to use PHPNomad’s updated versions and proper tagging. This drastically improves the reliability of the system and makes it easier for us to release versions as PHPNomad finds itself used in more contexts. What’s Changed Fix database package dependency issue by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/12 [...] This release is primarily focused on updating the dependencies in Siren to use PHPNomad’s updated versions and proper tagging. This drastically improves the reliability of the system and makes it easier for us to release versions as PHPNomad finds itself used in more contexts. ## What’s Changed - Fix database package dependency issue by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/12 - Added dependency by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/13 - Added new Siren logo by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/14 - Added North Commerce Integration by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/15 - Bump version 2.0.0-RC2 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/16 - Bump version 2.0.0 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/17 **Full Changelog**: https://github.com/Novatorius/siren-wordpress/compare/1.4.0…2.0.0 ## Release 2.0.2 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-2-0-2 What’s Changed Bump siren 2.0.2 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/21 Fixed https://github.com/Novatorius/api-manager-integration/pull/10 Full Changelog: https://github.com/Novatorius/siren-wordpress/compare/2.0.1…2.0.2 ## What’s Changed - Bump siren 2.0.2 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/21 - Fixed https://github.com/Novatorius/api-manager-integration/pull/10 **Full Changelog**: https://github.com/Novatorius/siren-wordpress/compare/2.0.1…2.0.2 ## Release 2.0.3 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-2-0-3 What’s Changed Bump 2.0.3 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/22 Fixed conversion creation issue using WooCommerce Integration. Reference - https://github.com/Novatorius/siren-extension-woocommerce/pull/11 Full Changelog: https://github.com/Novatorius/siren-wordpress/compare/2.0.2…2.0.3 ## What’s Changed - Bump 2.0.3 by @malayladu in https://github.com/Novatorius/siren-wordpress/pull/22 - Fixed conversion creation issue using WooCommerce Integration. Reference – https://github.com/Novatorius/siren-extension-woocommerce/pull/11 **Full Changelog**: https://github.com/Novatorius/siren-wordpress/compare/2.0.2…2.0.3 ## Release 3.0.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-3-0-0 Complete rewrite of the admin interface in React, standalone Collaborator Portal with branding, full REST API, LearnDash integration, and multi-tier plugin availability. Siren 3.0.0 is a complete rewrite. Programs, commissions, and payouts all work the same way they always have, but everything around them is new. ## New Admin Interface Every admin screen has been rebuilt in React. Programs, collaborators, conversions, obligations, fulfillments, transactions, distributors, and program groups all run as a single-page app inside WordPress admin. - Dark and light themes - Collapsible sidebar navigation - Help system with tooltips, slide-out drawers, and visual flow diagrams - Data tables with server-side filtering, sorting, and bulk actions - Detail views with lifecycle flow diagrams - CSV export for fulfillment payouts ## Collaborator Portal The collaborator dashboard is now its own standalone portal with separate navigation and branding. - Earnings summary (paid, unpaid, rejected) with obligation history - Engagement stats by program and time period - Referral URL generator with one-click copy - Coupon code display for assigned codes - Customizable colors via the block editor or shortcode attributes: background, text, accent, and sidebar logo - Picks up your theme's Global Styles automatically ## REST API Every resource now has a full REST API. If you're building integrations, automations, or headless setups, you can manage Siren entirely through API calls. - Full CRUD for all entity types - Field selection via `fields=` parameter - Bulk actions for obligations - Manual attribution endpoint for crediting collaborators via transactions - Event ingestion API for triggering conversions from external systems ## New Integrations - **LearnDash** – reward collaborators when learners complete courses or lessons - **Event Ingestion API** – send conversion events from any external system via REST, for headless and hybrid setups ## Recipe Import Siren now supports recipe import. These are pre-built program configurations you can apply in one click. Each import is tracked so you can see which recipe was applied, when, and by whom. ## Plugin Tiers Siren is available in multiple tiers: - **Siren Lite** (Free) – unlimited programs, affiliates, and conversions with percentage and fixed-per-transaction commissions - **Siren Essentials** – adds program groups, distributors, the standalone collaborator portal, advanced attribution models, lead tracking, and non-sale conversion events - **Siren Plus** – adds the collaborator group primitive, the flat structure, and group-bound eligibility - **Siren Pro** – adds hierarchical structures, layered walkers, and cascade payouts for engagements and metrics Collaborator groups and cascades arrived in the 3.3 line. For the full picture of what Plus and Pro add, see [release 3.3.0](/documentation/changelogs/release-3-3-0). ## Internationalization All frontend strings are now translatable, including the help content throughout the admin interface. ## Bug Fixes - Fixed engagement type format alignment between frontend and backend - Fixed line item filter categories to use slugs instead of numeric IDs - Fixed distributor schedule storage - Fixed program group initializer ordering - Fixed conversion event broadcasting on bulk approve/reject - Fixed draft obligations appearing in conversion detail views - Fixed fulfillment and payout endpoint paths - Fixed activity trends chart data mapping - Fixed cross-page navigation in WordPress admin ## Breaking Changes - The admin interface is now React-based. Custom PHP admin screen modifications from previous versions will need to be adapted. - The REST API uses a `fields=` parameter pattern instead of `include=extended`. - The Essentials plugin directory has changed from `siren` to `siren-essentials`. If you're updating from a previous version, deactivate the old plugin, install the new one, and reactivate. Your data is stored in the database and won't be affected. ## Release 3.0.1 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-3-0-1 Bug fixes for fulfillment amounts, database upgrades, license activation UI, and admin links. Adds PHPStan level 8 CI integration and Zod response adapters. ## Bug Fixes - Fixed fulfillment screen displaying amounts in cents instead of dollars (#36) - Fixed CREATE TABLE running before ALTER in InstallReportingService upgrade (#37) - License key activation now correctly reflects active status in UI (#33) - Fixed broken "Get Your License Key" link in WordPress admin (#31) ## Improvements - Added Zod response adapters at API boundary, removed ghost engagement type (#32) - Refactored collaborator action event listeners to own junction binding (#38) - Set up PHPStan level 8 with CI integration (#34) **Full Changelog**: https://github.com/Novatorius/siren/compare/3.0.0...3.0.1 ## Release 3.1.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-3-1-0 Activity feeds on every major record, plus fixes for fulfillment display, license activation, and obligation bulk actions. Siren 3.1.0 adds activity feeds across the admin and cleans up several smaller issues from 3.0.0. ## Activity Feeds Every major record in Siren now has an activity feed on its detail screen. Open a collaborator, conversion, obligation, fulfillment, engagement, or transaction and you'll find a chronological log of everything the system did to that record. Each lifecycle event lands on the feed as it happens, from a conversion being awarded through the obligation and payout that eventually settle it, including refunds that reverse the chain partway through. Siren's event pipeline records each lifecycle event automatically and links it to every related record, so a refund that reversed a conversion on Tuesday shows up wherever you look at it from, whether that's the collaborator, the transaction, or the obligation it voided. - Timestamped timeline on every major detail screen - Free-text search across the activity on a given record - Date-range filtering to narrow a specific period - Deep links to individual entries (`#note-{id}`) for sharing or bookmarking - Server-side pagination with infinite scroll on records with long histories - List-screen drawer that aggregates activity across every record of a given type - Distributor source type with its own searchable activity surface For a walkthrough of where the feed appears and how to use it, see the [activity feeds documentation](/documentation/general/activity-feeds). The feed is also exposed through the new [notes REST resource](/documentation/resource-reference/notes), which returns each entry with its blueprint key, rendered content, raw source data, and linked records. ## Bug Fixes - Fixed the fulfillment screen displaying amounts in cents instead of dollars. - Fixed the license key activation UI not reflecting the active state after a successful activation. - Fixed the broken "Get Your License Key" link in the WordPress admin. - Fixed a crash when selecting obligation checkboxes for bulk actions. - Fixed a CREATE TABLE / ALTER TABLE ordering issue in the reporting service upgrade path. - Normalized JSON response formatting across the REST API. - Removed a ghost engagement type that was exposed by the API but never actually fired. ## Release 3.2.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-3-2-0 Collaborator Portal fixes and a new Utilities tab in the WordPress admin. - Fixed the Collaborator Portal failing to load on fresh installs. - Fixed color settings on the Collaborator Portal block not applying to the live portal. - The Sidebar Logo setting on the Collaborator Portal block now uses the WordPress media picker. - Added a Utilities tab to the admin with logs, system status, and background repair actions. ## Release 3.3.0 Source: https://www.sirenaffiliates.com/documentation/changelogs/release-3-3-0 Collaborator groups arrive in Plus, with hierarchical structures and cascade payouts in Pro. Bind a group to a program or distributor to gate eligibility, and credit peers up or down a chain per layer. Siren 3.3.0 introduces collaborator groups. A collaborator group is a reusable set of collaborators (the people you pay, your affiliates among them) that you can bind to a program or distributor. The new Plus tier adds collaborator groups and a flat structure. The new Pro tier adds hierarchical structures and cascade payouts that credit peers across a chain. Everything in this release is additive and opt-in. If you run a standard affiliate program, nothing changes and there is nothing for you to do. The new features live in the new Plus and Pro tiers, and your existing programs keep paying out exactly as before. ## Collaborator Groups (Plus) The Plus tier adds the collaborator group as its own record in Siren that you create and reuse. A group holds a set of members and a structure that describes how those members relate to each other. - Create, edit, and delete groups from the admin, with name and description - Manage members independently of the group's structure - Flat structure, where every member is a peer with no hierarchy - Bind a group to a program or distributor to control who is eligible Binding a group to a program makes a collaborator eligible for that program when they are a member of the bound group. This runs alongside Siren's direct-binding eligibility, so a collaborator can become eligible through either path without one masking the other. The same binding works for distributors. When a collaborator is deleted, Siren removes them from every group they belonged to. The groups themselves are left in place. For an overview of the concept, see [what are collaborator groups](/documentation/general/what-are-collaborator-groups) and the [collaborator groups resource reference](/documentation/resource-reference/collaborator-groups). To create your first group, see [create a collaborator group](/documentation/getting-started/create-a-collaborator-group). ## Hierarchical Structures and Cascade Payouts (Pro) The Pro tier adds two hierarchical structures and the cascade calculation strategies that walk them. - Linear chain, where members are ordered by position - Parent-child, where members form a tree - Upline and downline cascade calculations for both engagements and metrics A cascade walks a hierarchical group from the triggering collaborator and credits their peers one layer at a time. The triggering collaborator is never credited by the cascade. Upline walks toward the top of the chain, and downline walks toward the leaves. Each layer has its own score, configured per layer from `pointsAtLayer1` through `pointsAtLayer5`. A cascade can reach up to five layers. The score is awarded per peer per layer, not split across the peers on that layer. A layer left at zero stops the cascade, so you signal the end of the payout by leaving the higher layers unset. Inactive peers are skipped without consuming the layer's slot, so the remaining peers on that layer still pay out at the full per-layer rate. To choose a structure, see [choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure). To choose a calculation, see [choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy). For a step-by-step setup, see [configure cascade payouts](/documentation/getting-started/configure-cascade-payouts). ## Why Some Calculation Methods Disappear This only affects programs and distributors that bind a collaborator group, and it does not change how your existing programs calculate payouts. Cascade calculations need a layered structure to walk. A flat group has no layers, so it cannot drive a cascade. When a flat group is bound, the calculation picker hides the cascade options, since there is nothing for them to walk. See [why some calculation methods disappear](/documentation/general/calc-capability-matching) for how the picker matches calculations to the structure that is bound. ## For Developers The collaborator group system is built to be extended. Custom structures, calculations, and eligibility resolvers register themselves by listening to registry events, with no changes required to Siren's core or the built-in tiers. - Register a custom structure resolver through the structure resolver registry event - Register a custom engagement or metric calculation through the calculation registry events - Register a custom eligibility resolver through the eligibility resolver registry events See [collaborator group structures](/documentation/extensions/collaborator-group-structures) and [walker capabilities](/documentation/extensions/walker-capabilities) for the extension points, and the [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) for the events each registry broadcasts. Switching an existing group's structure does not migrate per-member metadata. Membership rows are kept, but structure-specific keys such as a member's position or parent are not translated to the new structure. Resolvers tolerate unknown and missing keys, so a member with no position sorts to the top of a linear chain. ## Plugin Tiers Siren is now available in additional tiers: - **Siren Plus** adds collaborator groups, the flat structure, and group-bound eligibility - **Siren Pro** adds hierarchical structures, layered walkers, and cascade payouts for engagements and metrics ## Remove Collaborator Group Member Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/remove-member Removes a single collaborator from a collaborator group by collaborator id. # Remove Collaborator Group Member `DELETE /siren/v1/collaborator-groups/{id}/members/{collaboratorId}` Removes one collaborator from the group. The membership row is looked up by the group id and collaborator id from the path, then deleted. There is no soft-delete stage, so the removal is immediate and irreversible. Only the membership row is deleted. The collaborator record itself is untouched and stays available to other groups and programs. Requires authentication and the update capability on the `CollaboratorGroup` resource. ## Path Parameters | Parameter | Type | Required | Description | |---|---|---|---| | `id` | integer | Yes | The parent group's id. Taken from the path. | | `collaboratorId` | integer | Yes | The collaborator's id whose membership row is removed. Taken from the path. | This endpoint takes no request body. ## Example Request ``` DELETE /siren/v1/collaborator-groups/12/members/41 ``` ## Example Response ``` 204 No Content ``` Returns `204 No Content` on success. **Events:** Broadcasts `CollaboratorRemovedFromCollaboratorGroup` on success. **Error Responses:** - `404`. Either the membership row does not exist for that group and collaborator (message `Collaborator group member not found.`), or the group itself does not exist (message `The specified record does not exist.`, from the record-exists check that runs before the handler). The two messages let you tell a missing group from a missing membership. Removing a collaborator who is already not a member of an existing group returns the membership 404 rather than a 204, so a repeated or already-applied removal is reported as not-found rather than as a silent success. If the whole group is gone, you get the group 404 instead. - `500`. Database error while removing the member. ## RenewalTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce/renewal-triggered Fires when a subscription renewal payment completes. Traces back to the original purchase for continuing attribution. # RenewalTriggered `RenewalTriggered` fires when a subscription renewal payment completes. This is the most complex commerce event because it needs to trace back to the original purchase so that the same collaborator continues earning commissions on recurring revenue. The event ID is `renewal_triggered`, and its fully qualified class is `Siren\Commerce\Events\RenewalTriggered`. ## What does this event carry? The event carries the original `Transaction` model from the initial sale (not the renewal), an array of transaction details for the renewal order, and optional binding fields. The original transaction is the link back to the collaborator who earned the initial referral. By referencing it, the renewal process reuses the original attribution data instead of looking up a new opportunity. ```php use Siren\Commerce\Events\RenewalTriggered; $event = new RenewalTriggered( $originalTransaction, // Transaction: from the initial purchase $transactionDetails, // array: line items for the renewal $renewalOrderId, // ?int: external renewal order ID 'wc_order' // ?string: external type ); ``` ## How does the pipeline react? `InitializeRenewalConversion` in the conversions domain picks up this event and begins building renewal conversion records. The process mirrors the initial sale flow but uses the original transaction's attribution data rather than performing a fresh opportunity lookup. This ensures the collaborator who referred the original customer continues to receive credit. Renewal support is conditional in some extensions. WooCommerce, for instance, only binds the renewal transformer when WooCommerce Subscriptions is installed. Extensions that do not support subscriptions simply never fire this event. For the full lifecycle of commerce events and how they connect to the rest of the attribution pipeline, see the [Commerce Events overview](/documentation/developer-reference/events-commerce). ## Report a Refund Source: https://www.sirenaffiliates.com/documentation/resource-reference/events/refund POST /event/refund — reverse a previously reported sale by external ID. # Report a Refund `POST /siren/v1/event/refund` Creates a [`RefundTriggered`](/documentation/developer-reference/events-commerce/refund-triggered) event for a previously reported sale. The original transaction is located through Siren's [mapping table](/documentation/resource-reference/mappings) using the external ID and source — there's no need to know the Siren transaction ID. Used alongside [`POST /event/sale`](/documentation/resource-reference/events/sale) for headless commerce integrations that need to clean up conversions and obligations when an order reverses. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `source` | string | Yes | Same identifier used when the sale was reported (e.g., `"shopify"`). Combined with `externalId` to find the original transaction via the mapping table. | | `externalId` | string | Yes | The order or transaction ID that was refunded — must match the value passed when the sale was originally reported. | **Example Request:** ```http POST /wp-json/siren/v1/event/refund Content-Type: application/json { "source": "shopify", "externalId": "ORD-1004" } ``` **Example Response:** ``` HTTP/1.1 200 OK ``` Empty body on success. If no transaction can be found for the given external ID and source, returns `404` with the request fields echoed in the error body. This typically means either the sale was never reported through `/event/sale`, the IDs don't match, or the original sale belonged to a different `source` value. Internally this fires `RefundTriggered`, which traverses the conversion pipeline in reverse: linked conversions are rejected, obligations are cleaned up, and any associated payouts are flagged for review. See [How Refunds Work](/documentation/general/how-refunds-work) for the full lifecycle. ## Report a Sale Source: https://www.sirenaffiliates.com/documentation/resource-reference/events/sale POST /event/sale — create a SaleTriggered event from an external commerce system. # Report a Sale `POST /siren/v1/event/sale` Creates a [`SaleTriggered`](/documentation/developer-reference/events-commerce/sale-triggered) event from a checkout that did not run through a Siren-aware WordPress commerce extension. Used by the Siren Connect plugin and by custom bridges that route sales from an external commerce stack into Siren's attribution pipeline. The endpoint is purpose-built for the connector format. If you are integrating a new commerce platform from scratch, prefer building a Siren extension that fires `SaleTriggered` directly from the platform's webhook — the request shape here is constrained to the connector's payload, not a general-purpose sale envelope. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `source` | string | Yes | Identifier of the originating system — e.g., `"woocommerce"`, `"shopify"`. Used as the event source and to derive the binding type (`_order`). | | `externalId` | string | Yes | The order or transaction ID in the originating system. Used as the binding ID for refund matching. | | `total` | numeric | Yes | Order total. Multiplied by 100 internally to land in the smallest currency unit, so pass dollars as a decimal (e.g., `49.99`). | | `trackingId` | integer | Yes | The opportunity ID from the visitor's earlier site-visit tracking. Becomes the event's `opportunityId`. | | `currency` | string | No | ISO currency code. Defaults to `USD` if omitted. | | `items` | array | No | Line items: `[{ externalId, name, quantity, amount }]`. If provided, each item becomes a transaction detail entry; if omitted, a single `Order` line with the total is created. | **Example Request:** ```http POST /wp-json/siren/v1/event/sale Content-Type: application/json { "source": "shopify", "externalId": "ORD-1004", "trackingId": 4218, "total": 49.99, "items": [ { "externalId": "SKU-1", "name": "Annual Plan", "quantity": 1, "amount": 49.99 } ] } ``` **Example Response:** ``` HTTP/1.1 200 OK X-Siren-OID: 4218 Access-Control-Expose-Headers: X-Siren-OID ``` Empty body. The `X-Siren-OID` header echoes the resolved opportunity ID. Internally this fires `SaleTriggered`, which kicks off the standard conversion pipeline: programs are evaluated, conversions are awarded, and obligations are issued. See the [attribution pipeline](/documentation/resource-reference/pipeline-overview) for what happens after. ## Report a Site Visit Source: https://www.sirenaffiliates.com/documentation/resource-reference/events/site-visited POST /event/site-visited — create or update an opportunity for a referred visitor on a headless site. # Report a Site Visit `POST /siren/v1/event/site-visited` Creates a Siren opportunity for a referred visitor and runs the standard engagement-trigger pipeline against it. Used by headless frontends to report a `?ref=…` landing the way a WordPress page render would automatically. Internally broadcasts a [`SiteVisited`](/documentation/general/site-visited) event, so engagement triggers and downstream listeners behave identically to a native WordPress visit. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `collaboratorId` | integer or string | Yes | Numeric collaborator ID, or an alias reference in the form `:`. The most common alias form for tracking links is `tracking:` — the same code that appears in `?ref=…` URLs. Resolved by the alias-resolver middleware before validation. | | `userId` | integer | No | Platform user ID to associate with the visit, if known. | **Example Request:** ```http POST /wp-json/siren/v1/event/site-visited Content-Type: application/json { "collaboratorId": "tracking:REF123" } ``` **Example Response:** ``` HTTP/1.1 200 OK X-Siren-OID: 4218 Access-Control-Expose-Headers: X-Siren-OID ``` The body is empty. The opportunity ID is in the `X-Siren-OID` response header — read it client-side and persist it for the rest of the visitor's session. To continue an existing opportunity instead of creating a new one, send the existing ID in an `X-Siren-OID` request header on the call. Siren reads the header on the way in and uses it as the starting point for resolution. For the full headless flow — persistence patterns, conversion-time round-trip, and recommendations on cookies vs local storage — see [Headless Attribution](/documentation/headless/attribution). ## Reporting Source: https://www.sirenaffiliates.com/documentation/resource-reference/reporting REST API reference for the reporting query endpoint — entities, metrics, dimensions, and date ranges. # Reporting The reporting system provides a single query endpoint for retrieving pre-aggregated analytics data. Rather than computing reports on-the-fly from raw tables, Siren maintains four pre-aggregated tables that are updated incrementally as events occur. These tables are derived from [obligations](/documentation/resource-reference/obligations) and [engagements](/documentation/resource-reference/engagements). The query endpoint routes requests to the appropriate table based on the entity and dimensions requested. ## Endpoints All endpoints require authentication and are scoped to the current site. ### Query Reporting Data `POST /reporting/query` A unified query interface for all reporting data. The request specifies what entity to report on, which metrics to compute, how to group results (dimensions), and optional filters and date ranges. The middleware chain authenticates the request (verifying JWT and permissions), then `CollaboratorAliasResolverMiddleware` resolves any collaborator aliases in filter values. If the authenticated user is a collaborator rather than an admin, `AutoFilterByOwnerMiddleware` injects a `collaboratorId` filter scoping results to their own data. Finally, `ValidationMiddleware` validates the required fields. **Request Body:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `entity` | string | Yes | The data entity to query. See Supported Entities below. | | `metrics` | array | Yes | Array of metric expressions (e.g., `["sum:value"]`). | | `dimensions` | array | No | Array of dimension names for grouping results. | | `date_range` | string or array | No | Date range specification. See Date Ranges below. | | `filters` | array | No | Array of filter conditions. See Filter Syntax below. | | `order_by` | array | No | Ordering specification. | | `limit` | integer | No | Maximum number of result rows. | **Example Response:** ```json { "results": [ { "dimensions": ["USD", "pending"], "metrics": [5000] }, { "dimensions": ["USD", "approved"], "metrics": [12000] } ], "meta": { "entity": "obligations", "metrics": ["sum:value"], "dimensions": ["currency", "status"], "date_range": { "start": "2026-02-01", "end": "2026-02-28" }, "total_rows": 2, "truncated": false } } ``` Each result row contains a `dimensions` array (matching the requested dimension order) and a `metrics` array (matching the requested metric order). The `meta` object echoes the query parameters and includes the resolved date range. ## Supported Entities The `entity` field determines which pre-aggregated table is queried. The routing logic also considers the dimensions and metrics requested. ### `obligations` (Balances) Queried when entity is `obligations` and no time dimension is present. Routes to the **collaborator balances** table. Returns the sum of obligation values grouped by currency and status. **Dimensions returned:** `[currency, status]` **Example Request:** ```json { "entity": "obligations", "metrics": ["sum:value"], "dimensions": ["currency", "status"], "filters": [["is", "collaboratorId", [42]]] } ``` **Example Response:** ```json { "results": [ { "dimensions": ["USD", "pending"], "metrics": [5000] }, { "dimensions": ["USD", "approved"], "metrics": [12000] }, { "dimensions": ["USD", "complete"], "metrics": [45000] } ] } ``` Values are in cents (integer). Status values correspond to the obligation lifecycle: `pending`, `approved`, `complete`. ### `obligations` (Earnings Over Time) Queried when entity is `obligations` and a time dimension is present. Routes to the **collaborator earnings periods** table. Returns earnings totals grouped by currency and time period. **Time dimensions:** | Dimension | Period Type | Period Value Format | |-----------|-------------|---------------------| | `time:day` | day | `2026-04-06` | | `time:month` | month | `2026-04` | **Dimensions returned:** `[currency, periodValue]` **Example Request:** ```json { "entity": "obligations", "metrics": ["sum:value"], "dimensions": ["currency", "time:month"], "date_range": "6mo", "filters": [["is", "collaboratorId", [42]]] } ``` **Example Response:** ```json { "results": [ { "dimensions": ["USD", "2025-11"], "metrics": [8000] }, { "dimensions": ["USD", "2025-12"], "metrics": [12500] }, { "dimensions": ["USD", "2026-01"], "metrics": [9200] } ], "meta": { "date_range": { "start": "2025-10-06", "end": "2026-04-06" } } } ``` ### `engagements` (Activity Over Time) Queried when entity is `engagements`. Routes to the **collaborator activity periods** table. Returns engagement counts grouped by activity type and time period. **Dimensions returned:** `[activityType, periodValue]` **Example Request:** ```json { "entity": "engagements", "metrics": ["count"], "dimensions": ["activityType", "time:day"], "date_range": "30d", "filters": [["is", "collaboratorId", [42]]] } ``` **Example Response:** ```json { "results": [ { "dimensions": ["site_visit", "2026-03-15"], "metrics": [23] }, { "dimensions": ["site_visit", "2026-03-16"], "metrics": [18] }, { "dimensions": ["coupon_use", "2026-03-15"], "metrics": [3] } ] } ``` ### `obligations` (Percentile Standing) Queried when any metric starts with `percentile:`. Routes to the **collaborator standings** table. Returns pre-computed percentile rankings. Currently defaults to monthly period. **Dimensions returned:** `[]` (empty) **Example Request:** ```json { "entity": "obligations", "metrics": ["percentile:earnings"], "filters": [["is", "collaboratorId", [42]]] } ``` **Example Response:** ```json { "results": [ { "dimensions": [], "metrics": [85] } ] } ``` A percentile of 85 means the collaborator earns more than 85% of all collaborators. ## Date Ranges The `date_range` parameter supports multiple formats: ### Preset Strings | Preset | Description | |--------|-------------| | `7d` | Last 7 days. | | `30d` | Last 30 days. | | `{N}d` | Last N days. | | `month` or `this-month` | Current calendar month. | | `last-month` | Previous calendar month. | | `6mo` | Last 6 months. | | `{N}mo` | Last N months. | | `year` or `this-year` | Current calendar year. | | `last-year` | Previous calendar year. | | `previous:{preset}` | The period before the named preset (e.g., `previous:month`). | ### Explicit Range An array of two ISO 8601 date strings: ```json ["2026-01-01", "2026-03-31"] ``` ### Comparison Mode An object with `current` and `previous` keys (the endpoint resolves only the `current` range): ```json { "current": "month", "previous": "previous:month" } ``` ## Filter Syntax Filters are arrays of condition tuples: `[operator, field, values]`. ### `is` Operator Matches exact values. With a single value, generates an `=` clause; with multiple values, generates an `IN` clause. ```json [["is", "collaboratorId", [42]]] ``` ```json [["is", "currency", ["USD", "EUR"]]] ``` ### Allowed Filter Fields by Table Not all filter fields apply to every pre-aggregated table. Filters referencing columns that do not exist on the target table are silently ignored. | Table | Allowed Filter Columns | |-------|----------------------| | Balances | `collaboratorId` (all columns accepted) | | Earnings Periods | `collaboratorId`, `currency`, `periodType`, `periodValue` | | Activity Periods | `collaboratorId`, `activityType`, `periodType`, `periodValue` | | Standings | `collaboratorId`, `currency`, `periodType`, `periodValue` | ## Pre-Aggregated Tables The reporting system maintains four tables, updated incrementally by event listeners: | Table | Updated By | Trigger Events | |-------|-----------|---------------| | `collaborator_balances` | `UpdateBalanceOnObligationChange` | Obligation created/updated | | `collaborator_earnings_periods` | `UpdateEarningsPeriodOnObligationChange` | Obligation created/updated | | `collaborator_activity_periods` | `UpdateActivityPeriodOnEngagementCreated` | Engagement created | | `collaborator_standings` | Batch computation | Periodic recalculation | This architecture ensures that reporting queries are fast (simple SELECT on pre-aggregated data) at the cost of slightly delayed data (updated asynchronously as events fire). ## Access Control The `AutoFilterByOwnerMiddleware` automatically restricts results based on the authenticated user's role: - **Admin users:** See data for all collaborators. Can filter by any `collaboratorId`. - **Collaborator users:** Automatically filtered to their own data. The middleware injects a `collaboratorId` filter matching their collaborator record, overriding any client-supplied filter. This means the same endpoint serves both the admin dashboard (seeing all collaborators) and the collaborator portal (seeing only their own data). ## Relationship to Other Resources - **[Obligations](/documentation/resource-reference/obligations)** (upstream). The balances, earnings periods, and standings tables are all derived from obligation records. When an obligation is created or updated, event listeners incrementally update the corresponding pre-aggregated rows. Querying `entity: "obligations"` reads from these derived tables, not from the obligations table directly. - **[Engagements](/documentation/resource-reference/engagements)** (upstream). The activity periods table is derived from engagement records. Each time an engagement is created, `UpdateActivityPeriodOnEngagementCreated` increments the count for that [collaborator](/documentation/resource-reference/collaborators), activity type, and time period. - **Pre-aggregated tables.** Reporting never queries raw domain tables. The four pre-aggregated tables (`collaborator_balances`, `collaborator_earnings_periods`, `collaborator_activity_periods`, `collaborator_standings`) act as materialized views, trading write-time computation for read-time speed. This means reporting data may lag slightly behind the source records, but queries remain fast regardless of data volume. ## Resource Reference Introduction Source: https://www.sirenaffiliates.com/documentation/resource-reference/introduction How to access Siren's data through the REST API and PHP data layer. Covers authentication, base URLs, datastores, facades, and conventions. import CodeTabs from "@/components/content/CodeTabs.astro"; # Resource Reference Introduction This section documents every resource in Siren and how to access it. Each resource page covers the data model, status lifecycle, and relationships in one place, with code examples showing both the REST API and the PHP data layer side by side. Individual REST endpoint pages provide detailed HTTP request and response formats. ## REST API ### Authentication On WordPress, the API uses [WordPress's built-in authentication](https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/). Requests made through the WordPress admin (via the [REST API nonce](https://developer.wordpress.org/plugins/security/nonces/)) are authenticated automatically. All endpoints require authentication unless otherwise noted. Unauthenticated requests receive a `401` response. ### Base URL All Siren endpoints are registered under the `siren/v1` namespace using the [WordPress REST API](https://developer.wordpress.org/rest-api/). The full URL pattern is: ``` https://your-site.com/wp-json/siren/v1/{endpoint} ``` If your site uses a custom REST API prefix or has pretty permalinks disabled, adjust accordingly. The examples throughout this reference use the short form (`GET /siren/v1/programs`) without the site URL prefix. ### Response format Single-resource endpoints (get by ID, create, update) return the resource object directly as JSON with a `200` or `201` status code. List endpoints return a JSON array with an `x-siren-estimated-count` response header for total count estimation. See [Pagination, Filtering & Field Selection](/documentation/resource-reference/pagination-filtering-field-selection) for the full pagination, sorting, and filtering system. Errors return a JSON object with a `message` field and an appropriate HTTP status code. Simple errors include just the message: ```json { "message": "The requested resource could not be found." } ``` Validation errors (status `400`) include a `context` object with a `failedValidations` map. Each key is the field that failed, and the value is an array of error messages for that field. Multiple fields can fail at once, and a single field can have multiple validation errors: ```json { "message": "Validations failed.", "context": { "type": "VALIDATION_FAILED", "failedValidations": { "collaboratorId": ["This field is required."], "value": ["This field is required.", "Expected type: integer."], "status": ["Expected value to be one of: pending, fulfilled, cancelled."] } } } ``` | Status | Meaning | |---|---| | `400` | Bad request — validation failed or required parameters missing | | `401` | Unauthorized — authentication required | | `403` | Forbidden — authenticated but insufficient permissions | | `404` | Not found — the requested resource ID does not exist | | `500` | Internal server error | ### REST conventions **Field selection.** Most list and detail endpoints require a `fields` query parameter specifying which fields to include in the response. This keeps responses lean and avoids expensive joins for fields you don't need. Some fields are "extended" and only resolved when explicitly requested. Each resource's documentation lists its available fields. **Filtering and search.** List endpoints support filtering by resource-specific columns (like `status` or `type`) and text search via the `s` parameter. Filters use exact match by default. Passing comma-separated values creates an `IN` clause. See the [pagination and filtering guide](/documentation/resource-reference/pagination-filtering-field-selection) for details. **Two-stage deletion.** Resources that support deletion use a two-stage pattern. The first `DELETE` request sets the resource status to `deleted` (a soft delete). A second `DELETE` request on an already-deleted resource performs a permanent hard delete. This gives you a window to recover accidentally deleted records. **Bulk actions.** Several resources support bulk operations via a `POST /siren/v1/{resource}/bulk` endpoint. Bulk requests accept an `action` field and an `ids` array. Each resource's documentation lists its supported bulk actions. **Owner scoping.** Some endpoints (like obligations and reporting) automatically filter results based on the authenticated user's role. Admin users see all records. Collaborator users only see records associated with their own collaborator ID. This scoping is transparent — the same endpoint serves both roles with different result sets. **Collaborator alias resolution.** Several endpoints accept a `collaboratorId` parameter. Instead of passing a numeric ID, you can pass an [alias](/documentation/resource-reference/aliases) reference in the format `type:code` — for example, `tracking:JNE` or `coupon:SAVE20`. The middleware resolves the alias to the collaborator's numeric ID before the request is processed. ## PHP data layer ### When to access data directly Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Most records in the system are created as side effects of the normal event flow — a sale triggers a transaction, which triggers a conversion, which triggers an obligation. You should not manually create these records unless you have a specific reason to bypass the normal pipeline. The most common reasons to access datastores directly are reading data for display or reporting, looking up records to make decisions in custom logic, and building migration scripts that import data from other systems. If you find yourself creating transactions, conversions, or obligations manually, consider whether firing the appropriate domain event would be more correct. ### Accessing a datastore There are two ways to access any datastore: dependency injection (for extensions and any code running inside Siren's container) and facades (for external code like theme functions or standalone scripts). ```php use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore; class MyService { protected ProgramDatastore $programs; public function __construct(ProgramDatastore $programs) { $this->programs = $programs; } public function getActivePrograms(): array { return $this->programs->where([ ['type' => 'AND', 'clauses' => [ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]] ]); } } ``` ```php use Siren\Programs\Core\Facades\Programs; $activePrograms = Programs::andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); $program = Programs::getById(42); ``` Dependency injection is the preferred approach for extension code because it makes dependencies explicit and testable. Facades are convenient for one-off scripts, theme functions, or any code running outside Siren's initializer pipeline. If you find yourself using a facade inside extension code, that usually means the class isn't properly wired through the initializer system. ### Standard datastore methods Siren's datastores are built on [PHPNomad's datastore system](https://phpnomad.com/core-concepts/datastores/introduction). The two methods you'll use most often are `getById()` for fetching a single record by primary key and `andWhere()` for querying records with filter conditions. Both return model instances. ```php // Fetch a single program by ID. Throws RecordNotFoundException if missing. $program = $programs->getById(42); // Query with filter conditions. Returns an array of models. $active = $programs->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'] ]); ``` For writes, `create()` accepts an associative array of attributes and returns the new model. `update()` takes a primary key and an array of changed attributes. `delete()` removes a record by primary key. Filters use structured arrays with `column`, `operator`, and `value` keys. Supported operators are `=`, `!=`, `>`, `<`, `>=`, `<=`, `LIKE`, `NOT LIKE`, `IN`, and `NOT IN`. You can pass multiple clauses to `andWhere()` and they combine with AND logic. ```php // Multiple conditions $filtered = $programs->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'], ['column' => 'incentiveType', 'operator' => '=', 'value' => 'saleTransactionPercentage'] ]); // IN clause $specific = $programs->andWhere([ ['column' => 'id', 'operator' => 'IN', 'value' => [1, 2, 3]] ]); ``` Counting works with `count()` and `countAndWhere()`, which accept the same filter formats but return an integer instead of model arrays. Some datastores have additional domain-specific methods documented on their individual resource pages. ## Resources Each page in this section covers one resource, including the data model, access patterns (REST and PHP), and relationships to other resources. The resources follow the data flow through Siren's [attribution pipeline](/documentation/resource-reference/pipeline-overview): [Programs](/documentation/resource-reference/programs) define the incentive rules. [Program groups](/documentation/resource-reference/program-groups) bundle related programs so only one fires per conversion. [Collaborators](/documentation/resource-reference/collaborators) are the affiliates, partners, or participants who earn rewards. [Collaborator groups](/documentation/resource-reference/collaborator-groups) cluster collaborators under a structure that programs and distributors bind to so cascade calculations can walk the group and credit peers. When a customer interacts with a collaborator's referral link or coupon, Siren creates [opportunities](/documentation/resource-reference/opportunities) to track the visitor and [engagements](/documentation/resource-reference/engagements) to record confirmed attribution. If that customer completes a tracked action, a [conversion](/documentation/resource-reference/conversions) is created linking the engagement to a [transaction](/documentation/resource-reference/transactions). Approved conversions generate [obligations](/documentation/resource-reference/obligations). When it's time to pay, obligations are grouped into [fulfillments](/documentation/resource-reference/fulfillments) and [payouts](/documentation/resource-reference/payouts). For scheduled reward systems based on aggregate metrics, [distributors](/documentation/resource-reference/distributors) and [distributions](/documentation/resource-reference/distributions) handle the configuration and execution. Supporting resources include [aliases](/documentation/resource-reference/aliases) for tracking codes and coupon codes, [mappings](/documentation/resource-reference/mappings) for bridging Siren IDs with external platform IDs, and [configs](/documentation/resource-reference/configs) for the key-value configuration system. The [enums and constants](/documentation/resource-reference/enums-and-constants) reference lists the string identifiers used throughout the system. The [reporting](/documentation/resource-reference/reporting) endpoint provides aggregated analytics. [Notes](/documentation/resource-reference/notes) are the activity-feed entries that lifecycle events write and link to the records they touch. For a complete list of all REST endpoints, see the [All REST Endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## REST API Patterns Source: https://www.sirenaffiliates.com/documentation/extensions/wp-rest-api How register_rest_route maps to Siren's controller, middleware, and validation system. import CodeTabs from "@/components/content/CodeTabs.astro"; # REST API Patterns In WordPress, you register REST routes with `register_rest_route`, passing a namespace, a path, and an array of methods, callbacks, and permission callbacks. Siren replaces this with controller classes that implement typed interfaces for routing, middleware, and validation. ```php // WordPress: register a REST route with inline callbacks add_action('rest_api_init', function () { register_rest_route('my-plugin/v1', '/items', [ 'methods' => 'GET', 'callback' => function (WP_REST_Request $request) { $items = get_posts(['post_type' => 'item']); return rest_ensure_response($items); }, 'permission_callback' => function () { return current_user_can('manage_options'); }, ]); }); ``` ```php // Siren: a controller class with middleware and validation use PHPNomad\Rest\Interfaces\Controller; use PHPNomad\Rest\Interfaces\HasMiddleware; use PHPNomad\Rest\Interfaces\Request; use PHPNomad\Rest\Interfaces\Response; use PHPNomad\Validate\Interfaces\HasValidations; use PHPNomad\Validate\Interfaces\ValidationSet; use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore; class ListProgramsController implements Controller, HasMiddleware, HasValidations { protected ProgramDatastore $programs; public function __construct(ProgramDatastore $programs) { $this->programs = $programs; } public function getMethod(): string { return 'GET'; } public function getRoute(): string { return 'programs'; } public function getMiddleware(): array { return [ // Auth middleware checks permissions before the handler runs createAuthMiddlewareFromCurrentContext(), ]; } public function getValidations(): array { return [ new ValidationSet([ 'status' => ['string', 'in:active,draft,archived'], 'page' => ['integer', 'min:1'], ]), ]; } public function handle(Request $request): Response { $programs = $this->programs->andWhere([ ['column' => 'status', 'operator' => '=', 'value' => 'active'], ]); return new JsonResponse($programs); } } ``` ## How do controllers register? Controllers are registered via the `HasControllers` interface on an initializer. This is analogous to calling `register_rest_route` inside a `rest_api_init` callback, but declarative: ```php use PHPNomad\Rest\Interfaces\HasControllers; class MyInitializer implements HasControllers { public function getControllers(): array { return [ ListProgramsController::class, GetProgramController::class, UpdateProgramController::class, ]; } } ``` The framework resolves each controller from the DI container (so constructor injection works), then registers the route based on `getMethod()` and `getRoute()`. ## How does the middleware chain work? Middleware runs before your controller's `handle()` method, in the order you declare it. This replaces WordPress's `permission_callback` with a composable chain: 1. Authentication middleware verifies the user is logged in and has a valid token 2. Validation middleware checks request parameters against your validation rules 3. Owner scoping middleware restricts results to records the current user owns (for collaborator-facing endpoints) 4. Your handler runs only if all middleware passes If any middleware rejects the request, the chain stops and an appropriate error response is returned. Authentication failures return 401, authorization failures return 403, validation failures return 422 with field-level errors. ## What about validation? WordPress REST route validation uses `validate_callback` and `sanitize_callback` on individual args. Siren uses `ValidationSet` classes that define rules declaratively: ```php use PHPNomad\Validate\Interfaces\ValidationSet; class ListProgramsValidation implements ValidationSet { public function getRules(): array { return [ 'status' => ['string', 'in:active,draft,archived'], 'page' => ['integer', 'min:1'], 'limit' => ['integer', 'min:1', 'max:100'], ]; } } ``` When validation fails, the framework throws a `ValidationException` that the REST layer converts to a 422 response with field-level error details. You do not need to manually check parameters in your handler. ## Why is there no facade shortcut for controllers? Controllers are always registered through the DI system. Unlike events or config, there is no facade for REST route registration. This is intentional. Controllers depend on middleware, validation, and the full request/response lifecycle, which requires proper DI wiring. Facades are useful for simple lookups, but REST endpoints involve too many moving parts for a static wrapper to be practical. ## Where to go next For the full REST API conventions (route naming, response formats, pagination parameters, and CRUD patterns), see the [Resource Reference Introduction](/documentation/resource-reference/introduction). For the full PHPNomad framework documentation on REST, see [REST](https://phpnomad.com/core-concepts/rest/). ## Roles & Permissions Source: https://www.sirenaffiliates.com/documentation/extensions/wp-roles-permissions How current_user_can maps to Siren's action-based permission system and auth middleware. import CodeTabs from "@/components/content/CodeTabs.astro"; # Roles & Permissions In WordPress, you check permissions with `current_user_can('capability_name')`. Siren uses an action-based [permission system](https://phpnomad.com/core-concepts/auth/) where you check whether a user can perform a specific action (Create, Read, Update, Delete) on a specific model class. ```php // WordPress: string-based capability checks if (current_user_can('manage_options')) { // User is an admin } if (current_user_can('edit_post', $post_id)) { // User can edit this specific post } ``` ```php // Siren outside a REST request: resolve the current user and check actions use PHPNomad\Auth\Interfaces\CurrentUserResolverStrategy; use PHPNomad\Auth\Models\Action; use Siren\Programs\Core\Models\Program; use Siren\Auth\Core\Enums\ActionTypes; class MyService { protected CurrentUserResolverStrategy $userResolver; public function __construct(CurrentUserResolverStrategy $userResolver) { $this->userResolver = $userResolver; } public function checkAccess(): void { $user = $this->userResolver->getCurrentUser(); if ($user->canDoAction(new Action(ActionTypes::Update, Program::class))) { // User can update programs } if ($user->canDoAction(new Action(ActionTypes::Read, Program::class))) { // User can read programs } } } ``` ```php // Siren inside a REST controller: user is on the request, middleware handles auth use PHPNomad\Auth\Enums\ActionTypes; use PHPNomad\Auth\Models\Action; use PHPNomad\Rest\Exceptions\AuthorizationException; use PHPNomad\Rest\Interfaces\Controller; use PHPNomad\Rest\Interfaces\HasMiddleware; use PHPNomad\Rest\Interfaces\Request; use PHPNomad\Rest\Interfaces\Response; use Siren\Programs\Core\Models\Program; class UpdateProgramController implements Controller, HasMiddleware { public function getMiddleware(): array { return [ // Middleware checks auth before handle() runs // Returns 401 if not authenticated, 403 if not authorized createAuthMiddlewareFromCurrentContext(), ]; } public function handle(Request $request): Response { // If you reach here, the user is authenticated and authorized $user = $request->getUser(); // Additional checks if needed if (!$user->canDoAction(new Action(ActionTypes::Update, Program::class))) { throw new AuthorizationException('Cannot update programs'); } // ... handle the request } } ``` ## How does the action system work? Siren's permission model is built on two concepts: action types and model classes. `ActionTypes` is an enum with four values: | ActionType | Equivalent WordPress capability pattern | |---|---| | `ActionTypes::Create` | `create_posts`, `create_users` | | `ActionTypes::Read` | `read_post`, `list_users` | | `ActionTypes::Update` | `edit_post`, `edit_users` | | `ActionTypes::Delete` | `delete_post`, `delete_users` | The model class specifies what the action applies to. Instead of WordPress's string-based capability names like `'edit_post'`, Siren uses `new Action(ActionTypes::Update, Program::class)`. This is type-safe. Your IDE catches typos and you can refactor model class names without breaking permission checks. Under the hood, Siren maps these action/model pairs to WordPress capability strings. An action like `new Action(ActionTypes::Read, Program::class)` resolves to a capability string like `siren_action__read__Siren\Programs\Core\Models\Program`. WordPress's role system stores and checks these capabilities, so Siren's permission model layers on top of WordPress rather than replacing it. `Program::class` is one of many permissioned models. The same pattern applies to every model in Siren's managed set, including the Plus-tier `Siren\Plus\Core\Groups\Models\CollaboratorGroup`. So `new Action(ActionTypes::Update, CollaboratorGroup::class)` is a real capability check, the same as it is for programs. Full Create, Read, Update, and Delete on `CollaboratorGroup` is granted to the `administrator` and `siren_platform_manager` roles. Other `siren_*` roles do not hold model-level capabilities on `CollaboratorGroup` today. If you build a custom collaborator-group admin surface or REST controller, gate it on these actions and expect only administrators and platform managers to pass. For the full list of permissioned models, see [Enums & Constants](/documentation/resource-reference/enums-and-constants). ## How do REST controllers handle auth? Inside REST controllers, you typically do not check permissions manually. The `createAuthMiddlewareFromCurrentContext()` middleware handles authentication and authorization before your `handle()` method runs: 1. Authentication verifies the user has a valid auth token. Returns 401 if not. 2. Authorization checks that the authenticated user has the required capabilities for the controller's action. Returns 403 if not. By the time your handler executes, the user is guaranteed to be authenticated and authorized. Use `$request->getUser()` to access the authenticated user object for any additional logic. ## When do I need manual permission checks? Manual `canDoAction()` checks are useful in two cases: For conditional logic within a handler, the middleware grants access to the endpoint, but you may need finer-grained checks inside: ```php public function handle(Request $request): Response { $user = $request->getUser(); // The user can access this endpoint, but can they update THIS program? if (!$user->canDoAction(new Action(ActionTypes::Update, Program::class))) { // Show read-only view instead } } ``` For code outside REST controllers, listeners, services, and admin hooks do not have middleware. Use the `CurrentUserResolverStrategy` to get the current user and check permissions directly. ## Where can I learn more? For the full list of action types, model classes, and the generated capability strings, see [Enums & Constants](/documentation/resource-reference/enums-and-constants). For the full PHPNomad framework documentation on auth, see [Auth](https://phpnomad.com/core-concepts/auth/). ## Running a Subscription Affiliate Program Source: https://www.sirenaffiliates.com/documentation/getting-started/subscription-programs How to set up an affiliate program for a SaaS or subscription business, including trial-to-paid attribution, recurring commissions on renewals, plan upgrades, and refund handling. Subscription businesses have a few attribution and payout patterns that don't fit the standard "click, buy, commission" affiliate flow. Free trials blur when the conversion actually happens. Renewals create a new kind of recurring event that most affiliate programs ignore. Plan upgrades change the commission base mid-stream. And refunds on subscriptions behave differently depending on whether the customer cancels or downgrades. Siren handles all of these, but the configuration isn't obvious if you're new to the plugin. This page walks through the most common subscription-specific patterns and shows how to set them up. It assumes you already understand the basics of [creating a program](/documentation/getting-started/create-a-program-in-siren) and just need the subscription-shaped answers. If you're starting from scratch, the [quick start](/documentation/getting-started/quick-start) is a better first read. ## Trial-to-paid attribution The most common gotcha for SaaS affiliate programs is the gap between when a visitor signs up and when they pay. A free trial might last 14 days. An enterprise trial might stretch to 30 or more. The visitor's referral cookie could expire before the paid conversion happens, which would leave the affiliate unpaid for a conversion they actually drove. Siren handles this with the per-program cookie duration setting, covered in detail in [Cookie Duration and Attribution Windows](/documentation/general/cookie-duration). The idea is simple: set your program's expiration time to comfortably exceed your trial length. If you offer a 14-day trial, set the cookie to 30 days. If you offer a 30-day trial, set it to 60 days or more. You want the attribution window to be longer than the time it could plausibly take a visitor to convert from first click to first paid charge. When the paid subscription is detected (via your commerce plugin's order completion hook), Siren looks back to see if the visitor's cookie is still valid. If yes, the affiliate gets credit. If no, the conversion fires unattributed. There's no way to reach backward and re-attribute a conversion after the cookie has expired, so the conservative play is to set the cookie duration too long rather than too short. A practical rule of thumb: take your trial length, double it, and add a week. A 14-day trial gets a 35-day cookie. A 30-day trial gets a 67-day cookie. The buffer accounts for people who sign up for a trial, forget about it, come back to test again, and finally convert in the third week after their first visit. SaaS buying cycles are messy and the attribution window needs to accommodate that mess. One side effect: longer cookies mean more potential for attribution overlap if a visitor clicks multiple affiliate links. That's what the program group resolver is for. If you run multiple affiliate programs in a group and want the most recent click to win, use newest-binding-wins. If you want the first click to keep credit, use oldest-binding-wins. For SaaS programs specifically, newest-binding-wins is usually the right call because it gives credit to whoever got the customer over the finish line, not whoever introduced them to the product weeks earlier. ## Recurring commissions on renewals Siren can pay an affiliate on the initial sale and on each renewal of a subscription product. This requires two things: 1. A commerce integration that supports renewal tracking. WooCommerce Subscriptions, EDD Recurring Payments, and LifterLMS all support renewals. See the [Integration Feature Matrix](/documentation/general/integration-feature-matrix) for the full breakdown. 2. The renewal conversion type enabled in your program. Renewal conversions are an Essentials-tier feature. Once enabled, each renewal fires a Renewal conversion that runs through the same attribution pipeline as the original sale. The affiliate who got credit for the initial sale also gets credit for every renewal, for as long as the subscription stays active. If you want the affiliate to keep earning forever, you don't need any special configuration past turning on renewal conversions. If you want to cap the recurring commissions (say, after 12 months), you'd handle that by rejecting obligations past the cap during your fulfillment review. There's no built-in "stop paying after N renewals" setting today. Most SaaS programs that pay recurring commissions either pay forever or cap at 12 months. Paying forever is simpler operationally and makes the program more attractive to affiliates. Capping at 12 months is more common in lower-margin SaaS businesses where the lifetime value math gets tight if you keep paying out on the same customer for three years. Either works, and you can change your mind later by adjusting how you handle renewal obligations in your fulfillment reviews. ## Different rates on first sale versus renewals A very common pattern: pay 30% on the initial sale, 10% on each renewal. The initial sale is worth more because it represents new customer acquisition. Renewals are lower because the affiliate's contribution is smaller after the first sale. Siren handles this by running two programs in parallel, NOT in a program group. Here's the configuration. Your first program fires on Sale conversions only, with the 30% rate. This program triggers when the initial subscription is purchased and never again for that customer. Your second program fires on Renewal conversions only, with the 10% rate. This program triggers on every renewal charge after the first one and keeps firing for as long as the customer stays subscribed. Both programs target the same engagement triggers (link clicks, coupons, whatever the affiliate uses to drive traffic). When the initial sale happens, the first program fires. When the customer renews next month, the second program fires. The collaborator earns from both, which is exactly what you want. Keep these programs separate, not in a program group. Program groups are for resolving conflicts when multiple programs could claim the same conversion. Here, there's no conflict: the first sale is always a Sale conversion and never a Renewal, so the two programs never compete. ## Plan upgrades and downgrades When a customer upgrades from a $29/month plan to a $99/month plan, the next renewal commission is calculated against the new price. Siren reads the renewal amount from the commerce plugin's transaction data rather than from a stored snapshot of the original purchase, so upgrades and downgrades propagate automatically. The affiliate earns 10% of $99 on the next renewal instead of 10% of $29, without you touching any program settings. This also means the reverse is true for downgrades. If a customer drops from $99 back to $29, the next renewal commission is calculated against $29. There's no grace period and no snapshot behavior. Siren always pays against the actual charged amount, which is almost always what you want. If you want a different commission rate based on plan tier (for example, higher commission on enterprise plans), you'd need to use [line item filters](/documentation/general/line-item-filters) by SKU and run separate programs per tier. This is more complex than most subscription programs need, and most operators stick with a single rate across all plans. It's an option if you really need it, and Beacon can walk you through the line item filter configuration if you want to go that route. ## Trial expirations without conversion If a trial ends and the customer never pays, no conversion fires and no commission is owed. The original engagement (the click that brought the visitor to the trial signup) is still on file, but it never produces a payout. This is the right behavior: you don't want to pay affiliates for trials that never convert. If you want to track "trials started" as a metric, or if you want to pay affiliates a small bounty for every trial signup in addition to the bigger commission on conversion, run a Lead conversion program in parallel. Lead conversions fire on a signup event rather than a sale, and they're an Essentials-tier feature. The two-program structure here is: a Lead program pays (say) $1 per trial started, and a Sale program pays the full 30% on conversion to paid. The affiliate gets both when a paid conversion happens and just the $1 when a trial doesn't convert. This is useful if you have affiliates driving high trial volume but lower conversion rates, because it rewards the upstream work even when the downstream conversion doesn't happen. It's less useful if your trial conversion rate is high, because the added complexity of running two programs isn't worth the small bounty. ## Tracking who actually drove the sale For SaaS products with long buying cycles, one of the hardest questions is whether an affiliate actually drove a sale or whether the customer was going to buy anyway. A visitor who was already considering your product clicks an affiliate link out of convenience and then buys the product they were going to buy regardless. Siren has no way to distinguish this from a "real" affiliate-driven sale, and frankly neither does any other affiliate platform. The pragmatic answer is to trust the attribution within your cookie window and accept that some percentage of affiliate commissions are going to customers who would have converted on their own. This is a cost of running an affiliate program, not a defect in the tracking. If the total commission you pay is less than the total new revenue the program generates, the program is working even if some of the individual attributions aren't "correct." If you're seeing an unusually high percentage of affiliate conversions from a single affiliate and suspect they're gaming the system (clicking their own link before known customers complete purchases), the fix is to investigate and manually reject suspicious conversions. Siren's conversion rejection is one-click, so cleaning up bad attributions is quick. ## Annual versus monthly subscriptions If you sell both annual and monthly plans, think about how you want to handle the difference in affiliate payouts. An annual plan at $240/year generates a single large commission at signup (30% of $240 = $72), while a monthly plan at $20/month generates a smaller commission at signup ($6) plus renewal commissions over time. Most SaaS programs pay the same percentage on both plans and let the math work out naturally. The affiliate earns the $72 upfront on annual customers and the $6 + 10% renewals on monthly customers, and over a year or two the totals tend to converge. This is the simplest approach and the one most affiliates expect. If you want to encourage annual sales specifically (because annual customers have lower churn and are more valuable to your business), you can offer a higher rate on annual plans via line item filters. Set a 40% rate on any line item with "annual" in the SKU, and 30% on everything else. This incentivizes affiliates to push annual plans and gives you a cleaner lifetime value picture. ## Add-ons and cross-sells SaaS products often sell add-ons alongside the main subscription. Extra user seats, additional storage, premium support, training hours, a second product bundled with the first. The question is whether affiliates should earn commission on these add-ons. There's no universal answer. If you want affiliates to earn on everything, just use a single program with no line item filters. Every purchase (main plan, add-on, anything with a price) generates a conversion and a commission. If you want affiliates to earn only on the main plan, use line item filters by SKU to exclude add-on products. The affiliate gets credit for the $99/month plan and no credit for the $20 extra seat. This is the cleaner approach if your add-ons have thin margins or if they're typically added by existing customers rather than purchased during initial signup. If you want different rates on add-ons versus main plans, run two programs with line item filters: one that includes only main plan SKUs with a 30% rate, another that includes only add-on SKUs with a 10% rate. This is more configuration work but gives you precise control over what each affiliate earns on what. ## Subscription refunds Siren's refund pipeline, covered in [How Refunds Work](/documentation/general/how-refunds-work), handles full subscription cancellations the same way as one-time purchase refunds. The conversion is rejected, and the obligation is rejected too if it hasn't been paid out yet. If the obligation has already been fulfilled, Siren leaves it alone and you handle recovery manually. Partial refunds are where subscription handling gets awkward. If a customer downgrades mid-cycle and gets a prorated credit, Siren does NOT automatically reduce the commission. The conversion stays at full value. You'd need to manually adjust the affected obligation. This is a known limitation. If you issue a lot of prorated refunds, the simplest workaround is to delay fulfillment until your refund window has closed, then review the obligations for adjustments before sending payouts. A seven-day review window before each monthly payout catches most of the prorated refund cases and keeps the manual work to a minimum. Cancellation of a subscription future renewal (where the customer stops auto-renewing but keeps access through the current period) is not a refund. No conversion is rejected, no obligation is touched. The affiliate keeps credit for the sales that already happened, and future renewals simply stop firing because there are no future renewals to charge. This is the right behavior, because the affiliate did earn the commissions that have already been paid, and the customer cancellation is unrelated to the quality of the referral. ## Recommended starting points For a SaaS business launching its first affiliate program, start with the [basic affiliate program recipe](/recipes/basic-affiliate-program) and adjust the cookie duration to exceed your trial length. That gets you a working 20-30% commission on new sales with standard click attribution. It's the quickest path from zero to a functioning program, and you can layer on renewal commissions and lead bounties later. For a setup with separate first-sale and renewal commission rates, use the [business partner revenue share recipe](/recipes/business-partner-revenue-share) or the [channel partner program recipe](/recipes/channel-partner-program) as a starting point, then add a second program for the renewal rate. Both recipes ship with percentage-of-transaction incentives that are easy to duplicate and modify. The channel partner recipe also includes manual attribution, which is useful if you have some deals closed through direct sales conversations rather than tracking links. If the patterns above don't cover your situation, describe your requirements to [Beacon](/documentation/getting-started/what-is-beacon) and ask it to generate a custom recipe. Beacon understands the subscription-specific constraints and can produce a fully-configured recipe matching your exact trial length, commission rates, and refund policies. For deeper reference, the two most useful pages for subscription programs are [Cookie Duration and Attribution Windows](/documentation/general/cookie-duration) and the [Integration Feature Matrix](/documentation/general/integration-feature-matrix). The first tells you how to size your attribution window. The second tells you which features actually work on your commerce stack. If you're on a combination that doesn't support renewal conversions, you'll need to either change your setup or accept that you're running a first-sale-only program. ## SaleTriggered Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce/sale-triggered The primary commerce event for purchases. Fires when an extension detects an order that should be tracked by Siren. # SaleTriggered `SaleTriggered` is the primary commerce event for purchases. It fires when an extension determines that an order should be tracked by Siren, and it sets the entire downstream pipeline in motion. Conversions, obligations, and eventually payouts all trace back to this event. The event ID is `sale_triggered`, and its fully qualified class is `Siren\Commerce\Events\SaleTriggered`. ## What does this event carry? The event carries five pieces of data: the opportunity ID that links the sale back to the affiliate referral, an array of transaction details describing the line items (each with a name, description, value, and type), a source string identifying which extension produced the event, and two optional binding fields. The `bindingId` maps the event to the external platform's order record, and `bindingDataType` identifies the kind of external record it maps to. ```php use Siren\Commerce\Events\SaleTriggered; $event = new SaleTriggered( $opportunityId, // int: the opportunity that tracked this referral $transactionDetails, // array: line items with name, description, value, type 'wc', // string: source extension identifier $orderId, // ?string: external order ID for mapping 'wc_order' // ?string: external type identifier ); ``` ## How does the pipeline react? When `SaleTriggered` fires, `InitializeSaleConversion` in the conversions domain picks it up and begins the conversion process. During initialization, the system fires a separate `SaleInitialized` event (event ID: `sale_initialized`). This is an internal coordination point. Listeners that need to act during the initialization phase, before conversions are fully built, subscribe to `SaleInitialized` rather than to `SaleTriggered` itself. The two events represent different phases of the same process: `SaleTriggered` is the entry point from the extension, and `SaleInitialized` is the internal signal that initialization is underway. For the full lifecycle of commerce events and how they connect to the rest of the attribution pipeline, see the [Commerce Events overview](/documentation/developer-reference/events-commerce). ## Self-Referral Prevention Source: https://www.sirenaffiliates.com/documentation/general/self-referral-prevention How Siren prevents collaborators from earning commissions on their own purchases, what the mechanism actually catches, and the limitations to be aware of. Siren has a built-in, always-on mechanism that prevents collaborators from earning commissions on their own purchases. It runs automatically without any configuration, and you can't turn it off. This page explains how it works, what it catches, and the situations where it won't save you. ## How it works When a collaborator (any user with a Siren role) logs into the site, Siren detects the login through the `UserLoggedIn` event. A listener checks whether the user is a Siren user, and if they are, it invalidates any active [opportunity](/documentation/general/what-is-an-opportunity) tied to that user. Once an opportunity is marked invalid, no [engagements](/documentation/general/what-is-an-engagement) or [conversions](/documentation/general/what-is-a-conversion) can be created from it. In practice, this means a collaborator who logs in and then makes a purchase won't earn a commission on that purchase, regardless of which referral link or coupon code they used. The login kills the opportunity before the rest of the attribution pipeline has a chance to act on it. ## What it catches The most common case this protects against is a collaborator browsing their own site while logged into their account, clicking their own affiliate link to test it (or to buy something), and completing a purchase. The login event invalidates the opportunity before any engagement can fire, so the commission never materializes. It also catches multi-session scenarios. If the collaborator was logged in earlier in the day, then opened a new tab and started the purchase flow, the opportunity tied to their user gets invalidated as soon as Siren sees the login, even if the buyer-side activity happens later. The check runs against the user's opportunity, not against a specific session. ## The limitations The mechanism has real gaps. You should know about them before you rely on it for high-value programs. ### Guest checkout If the collaborator makes a purchase as a guest without logging into their Siren account, the invalidation never triggers. There's no `UserLoggedIn` event for that session, so the listener never runs and the opportunity stays active. A determined collaborator can circumvent the protection by using their own link in an incognito browser and checking out without logging in. ### Different account email If the collaborator checks out with a different email address that isn't linked to their Siren user account, the system won't recognize them as a self-referral. The invalidation hangs off the user ID, so a checkout under an unrelated email looks like any other customer. ### No per-program toggle The mechanism is always-on and global. There's no way to disable it for a specific program, and there's no way to add additional checks (like a shipping address match or an email domain match) through the built-in settings. What you see is what you get. ## When you might want stronger protection For high-payout programs where a determined collaborator could justify the effort of using a guest checkout, combine Siren's automatic protection with manual review. Hold conversions in pending status (configurable per program) and review each one before approving. Flag anything that looks like the collaborator's own pattern: the same email domain as the collaborator's account, the same shipping address on file, or purchase timing that lines up suspiciously with the collaborator's own activity. This isn't something Siren can do for you, because the signals that would catch it vary by business. But the hold-and-review pattern gives you a human checkpoint that the automatic mechanism can't. ## Related fraud prevention features Siren has a couple of other protections worth knowing about. Duplicate transaction binding prevents the same order from being processed twice, so an attacker can't replay an order to earn multiple commissions. Lead deduplication within the program expiration window prevents the same lead from being credited twice to the same collaborator during an active tracking window. Neither of these is a self-referral check, but together with the login-based invalidation they cover most of the common abuse patterns. For how refunds interact with the commission pipeline, see [How Refunds Work](/documentation/general/how-refunds-work). For the broader picture of what fraud patterns you're likely to see and how to catch them without paying for a dedicated fraud service, read [how to spot and prevent affiliate fraud](/blog/how-to-spot-and-prevent-affiliate-fraud). ## For developers The listener that handles this is `InvalidateOpportunitiesFromSirenUsers` at `lib/Opportunities/Core/Listeners/InvalidateOpportunitiesFromSirenUsers.php`. It subscribes to the `UserLoggedIn` event, resolves the user's active opportunity through the mapping datastore, and delegates to `OpportunityInvalidationService`, which sets the opportunity status to `invalid` and broadcasts an [`OpportunityInvalidated`](/documentation/developer-reference/events-attribution/opportunity-invalidated) event. The "is this a Siren user?" check is the `SirenUserValidationStrategy`, which iterates the configured Siren roles and returns true if the user has any of them. ## Set Collaborator Group Members Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/set-members Full-replaces the member set of a collaborator group, reconciling it against the payload. # Set Collaborator Group Members `PUT /siren/v1/collaborator-groups/{id}/members` Full-replaces the member set of an existing collaborator group. The group must exist (enforced by `RecordExistsMiddleware`). Requires authentication and the update capability on the `CollaboratorGroup` resource. The endpoint reconciles the group's full member set against the payload. Collaborators present in the body but not in the group are added, collaborators in the group but missing from the body are removed, and an existing member's metadata is rewritten only when the submitted value differs from the stored one. Each entry's `metadata` is structure-specific and follows the same rules as the `CollaboratorGroupMember` resource. An entry that omits `metadata` is treated as empty metadata, so submitting an existing member without a `metadata` object clears whatever metadata that member had, subject to the same only-on-change rule: clearing an already-empty member writes nothing and fires no event. Entries with a missing or zero `collaboratorId` are skipped, and when the same `collaboratorId` appears more than once the last entry wins. Because this is a full replace, the `members` array is the complete desired roster rather than a delta. Sending an empty `members` array, omitting it, or sending a body whose entries all get skipped removes every member from the group. That makes the destructive path the default path, so treat a missing or malformed `members` field as a roster wipe rather than a no-op. To add or remove a single member without restating the whole roster, use [Add members](/documentation/resource-reference/collaborator-groups/add-members) or [Remove member](/documentation/resource-reference/collaborator-groups/remove-member) instead. Retrying the same complete payload is safe and converges on the same roster. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `members` | object[] | No | The full desired member set. Each entry is `{ "collaboratorId": int, "metadata": object? }`. Defaults to an empty array, which removes all members. | **Query Parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `id` | integer | Yes | The collaborator group's id. Taken from the path. Must reference an existing group. | **Example Request:** ```json { "members": [ { "collaboratorId": 41, "metadata": { "position": 1 } }, { "collaboratorId": 42, "metadata": { "position": 2 } } ] } ``` **Example Response:** ```json [ { "id": 87, "groupId": 12, "collaboratorId": 41, "metadata": { "position": 1 } }, { "id": 88, "groupId": 12, "collaboratorId": 42, "metadata": { "position": 2 } } ] ``` Returns the reconciled member array with a `200` status. Each returned row uses the same adapter shape as [Add members](/documentation/resource-reference/collaborator-groups/add-members): `id`, `groupId`, `collaboratorId`, `metadata`, `dateCreated`, and `dateModified`. The example above shows the first four for brevity. **Error Responses:** - `404`. No collaborator group found with that ID. - `500`. Database error while updating members. **Events:** Reconciliation broadcasts `CollaboratorAddedToCollaboratorGroup` for each newly added row, `CollaboratorGroupMemberMetadataChanged` for each existing row whose metadata changed, and `CollaboratorRemovedFromCollaboratorGroup` for each row dropped from the group. ## Set Up a Program Registration Form Source: https://www.sirenaffiliates.com/documentation/getting-started/set-up-a-program-registration-form Automatically register collaborators with a registration form. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; import StepList from "@/components/content/StepList.astro"; ## What a registration form does Registration forms let potential [collaborators](/documentation/general/what-is-a-collaborator) sign up for your [programs](/documentation/general/what-are-programs) directly from your site. Instead of [creating each one manually](/documentation/getting-started/managing-collaborators-affiliates), visitors fill out a form, verify their email, and get added to whichever programs you've configured. This is how you run an open program without approving every applicant by hand. ## Adding a form to your site Create a new page (or edit an existing one) in the WordPress block editor. Click the inserter, search for "Siren," and add the Siren Collaborator Registration block. The block's sidebar settings control what happens on submission. The collaborator status setting decides whether new signups land in Pending (held for your approval) or Active (creating [engagements](/documentation/general/what-is-an-engagement) immediately). The "Approve existing collaborators" toggle controls cross-program signups: when checked, someone already in another program is added to this form's programs automatically, and when unchecked you get an email asking you to review the request. Program selection picks which programs new signups enroll in, and you can select one or several. You can also customize the confirmation message that appears after submission. Publish when you're done. ## What the collaborator sees The form asks for a nickname, full name, and email. After they submit, Siren sends a confirmation email to verify the address is real. This prevents anyone from signing up with someone else's email. The email contains a link. Clicking it creates (or updates) the collaborator account and shows a simple confirmation page. If you set the status to Pending, they'll sit in Siren until you activate them. If Active, they can start sharing referral links immediately. ## Finding new registrations New signups appear on the Siren > Collaborators screen alongside any you've created manually. If you're using Pending, you'll review registrations here and activate them individually or in bulk via the "Activate" action. Activation doesn't send an automatic notification, so reach out separately if you have an onboarding flow. ## Multiple forms for different programs Each registration block is configured independently. If you have an affiliate program open to anyone and a separate blog content program for writers, create two pages with two different forms enrolling into each. The "Approve existing collaborators" setting shines here. When someone already active in your affiliate program fills out the blog content form, Siren can add them to that second program automatically without a second approval round. The video walks through creating a registration page with the Siren Collaborator Registration block. It covers the three main settings: collaborator status (Pending vs. Active), the "Approve existing collaborators" checkbox for cross-program signups, and the program selection for deciding which programs new signups join. It then demonstrates the visitor experience: filling out the form, receiving a confirmation email, and clicking the verification link to finalize the collaborator record. The new collaborator appears on the Siren > Collaborators screen with a Pending status, ready for activation. The video closes with a multi-form scenario. A separate page with its own form is set up for a blog content program. With "Approve existing collaborators" checked, an existing affiliate signing up through the second form is added to the blog content program automatically. ## Set Up Affiliate Coupons Source: https://www.sirenaffiliates.com/documentation/getting-started/set-up-affiliate-coupons How to assign coupon codes to collaborators so they can be tracked at checkout. Covers WooCommerce, Easy Digital Downloads, LifterLMS, and NorthCommerce. import StepList from "@/components/content/StepList.astro"; This guide walks you through assigning a coupon code to a collaborator so that Siren can credit them whenever a customer uses that code at checkout. It applies to WooCommerce, Easy Digital Downloads, LifterLMS, and NorthCommerce. The setup is almost identical on each platform, but the location of the coupon screen changes, so we've called out the differences below. Coupon tracking is especially useful for situations where a clickable referral link isn't practical. Think podcast sponsorships, printed flyers, YouTube video descriptions, or any social post where the audience is more likely to remember a short code than click a URL. Before you start, make sure the collaborator belongs to a [program](/documentation/general/what-are-programs) that has the [Coupon Code Used](/documentation/general/coupon-code-used) engagement trigger enabled. If that trigger isn't turned on, applying the coupon at checkout won't produce any attribution, no matter how it's assigned. For a conceptual overview, see [Coupon Code Tracking](/documentation/general/coupon-tracking). ## How it works Siren doesn't create coupons for you. You build the coupon inside your commerce plugin the way you normally would, choosing the discount type, amount, restrictions, and expiration. Siren adds a small "assign to a collaborator" field to the coupon edit screen. Once you pick a collaborator and save, that code is linked to their account. From there, the normal attribution pipeline takes over. When a customer applies the code at checkout, Siren creates a Coupon Code Used [engagement](/documentation/general/what-is-an-engagement) for the collaborator who owns the code. If the customer completes the purchase, that engagement becomes a [conversion](/documentation/general/what-is-a-conversion), and the program generates an [obligation](/documentation/general/what-are-obligations) recording what the collaborator is owed. For the concept-level explanation and a rundown of which integrations detect coupons differently, see [Coupon Code Tracking](/documentation/general/coupon-tracking). ## Setting up a coupon in WooCommerce WooCommerce is the most common setup, so we'll walk through it in detail. The same logic applies to the other integrations covered below. Coupons in the WordPress admin", description: "This opens WooCommerce's coupon management screen." }, { title: "Create a new coupon or edit an existing one", description: "Pick a code your collaborator will share with their audience, then set the discount type and amount." }, { title: "Find the Siren section on the coupon edit screen", description: "Siren adds an 'assign to a collaborator' field below the standard WooCommerce settings." }, { title: "Select the collaborator", description: "Start typing their name and pick them from the dropdown." }, { title: "Publish or update the coupon", description: "The code is now linked to that collaborator's account." }, ]} /> You can confirm the assignment by opening Siren > Collaborators, clicking the collaborator's name, and checking the Coupons tab on their profile. ## Other platforms The pattern on every other supported integration is the same: find the Siren collaborator field on the coupon edit screen, pick a collaborator, and save. Only the location of the coupon screen changes. ### Easy Digital Downloads EDD has its own coupon system under Downloads > Discounts. Open or create a discount, scroll to the Siren collaborator field that Siren adds to the discount edit screen, select the collaborator, and save. EDD detects the coupon the moment it's applied at checkout, the same way WooCommerce does, so the engagement fires before the order is placed. ### LifterLMS LifterLMS stores coupons under LifterLMS > Orders > Coupons. Create or edit a coupon, set the title, discount, and any restrictions, then pick the collaborator in the Siren assignment field before publishing. One thing to know about LifterLMS: it doesn't expose a hook for the moment a coupon is applied at checkout, so Siren detects the coupon when the sale completes instead. The engagement still gets created and the collaborator still gets credit, but it fires alongside the conversion rather than a step earlier. For most use cases this is invisible. See the [integration feature matrix](/documentation/general/integration-feature-matrix) for the full comparison. ### NorthCommerce NorthCommerce has a coupons screen in its own admin area. Create or edit a coupon there, find the Siren collaborator field, select a collaborator, and save. NorthCommerce detects coupons at checkout the same way WooCommerce and EDD do. A reminder for anyone shopping integrations: LearnDash and Gravity Forms don't have coupon systems, so they don't support coupon tracking. If your store runs on one of those plugins, you'll need to rely on referral links or another engagement trigger instead. ## How attribution works when a link and a coupon are both used Coupons get applied at checkout, which is usually the last thing a customer does before placing an order. That matters because most affiliate programs use the [newest engagement wins](/documentation/program-group-structures/newest-engagement-wins) sorter, and the coupon code is almost always the newest engagement on the record. So if a customer originally arrived through Affiliate A's referral link but entered Affiliate B's coupon code at checkout, Affiliate B gets credit for the conversion inside that program group. The link visit still happened and is still stored, but the coupon is the most recent engagement, so it wins. This is by design. Entering a coupon is a deliberate action the customer took at the point of purchase, and it's the most direct signal of which collaborator influenced the sale. If you run multiple programs in different [program groups](/documentation/general/what-are-program-groups), each group evaluates independently. A single transaction can still generate multiple [conversions](/documentation/general/what-is-a-conversion) and [obligations](/documentation/general/what-are-obligations) across different groups. An instructor royalty program and an affiliate program can both pay out on the same sale, for example, because they live in separate groups. The coupon only competes with other engagements inside its own group. If you want more background on how sorters pick a winner when several engagements are present, take a look at [what is an engagement](/documentation/general/what-is-an-engagement) and the [newest engagement wins](/documentation/program-group-structures/newest-engagement-wins) program group structure. ## Testing your setup The fastest way to verify everything is wired up correctly is a test purchase. Conversions", description: "You should see a new conversion tied to the test transaction. If you used a manual payment, mark the order completed so the conversion finalizes." }, { title: "Open the conversion and verify attribution", description: "Confirm that the test collaborator received credit and that an obligation was created for them." }, ]} /> If the conversion doesn't appear, double-check that the program has the Coupon Code Used trigger enabled and that the test collaborator is actually part of that program. Those are the two things that cause this to silently fail. It's also worth confirming the coupon is actually saved with an assignment, since it's easy to miss the dropdown on a busy coupon edit screen. ## Where to go next Once you've got coupons working, there are a few natural next steps depending on where you are in your setup: - [Managing collaborators](/documentation/getting-started/managing-collaborators-affiliates) covers how to keep their profiles, contact info, and program memberships organized. - [How to pay collaborators](/documentation/getting-started/how-to-pay-collaborators) walks through turning obligations into real payouts once sales start rolling in. - The [coupon-based influencer program recipe](/recipes/coupon-based-influencer-program) is a solid starting template if you're building a program where the coupon is the main tracking method. - The [integration feature matrix](/documentation/general/integration-feature-matrix) is handy if you run more than one commerce plugin and want to know exactly which features are available where. - [How to spot and prevent affiliate fraud](/blog/how-to-spot-and-prevent-affiliate-fraud) covers coupon stacking specifically, which is the main abuse pattern coupon-tracked programs face. ## Shared Engagement Pool (Distribution Structure) Source: https://www.sirenaffiliates.com/documentation/distribution-structures/shared-engagement-pool A distribution structure where the reward pool is divided equally among all collaborators who accumulated any metric score during the period. The Shared Engagement Pool is a distribution structure where the reward pool is divided equally among all collaborators who earned any metric score during the distribution period. It does not matter how high each collaborator's score is. If they have any score at all, they receive an equal share. This is the simplest distribution structure. When a distribution triggers on schedule, Siren looks at every collaborator who has a non-zero metric score, divides the total reward pool by the number of qualifying collaborators, and creates an [obligation](/documentation/general/what-are-obligations) for each one. A collaborator who drove 50 site visits gets the same share as one who drove 500. The only question is whether they contributed at all. ## How the reward pool is calculated The reward pool for a distribution is based on a percentage of revenue collected since the last distribution. You configure this percentage when setting up the [distributor](/documentation/general/what-are-distributors). For example, if you set the pool to 10% and your site earned $50,000 since the last distribution, the reward pool for that period is $5,000. With the Shared Engagement Pool, that $5,000 would be split equally among all collaborators who have a metric score. If 10 collaborators qualified, each receives a $500 obligation. ## How metric scores work in this structure Each distributor is configured with [tracking events](/documentation/general/what-are-distributors#tracking-events-and-metric-values) that define what it measures. As these events happen during the distribution period, Siren accumulates a metric score for each collaborator. Each event type has a configurable point value. In the Shared Engagement Pool, these scores only determine eligibility. A collaborator needs at least one tracked event to qualify. Beyond that threshold, the score has no effect on payout size. This makes the structure straightforward but also means that collaborators who contribute heavily receive the same reward as those who contribute minimally. Cascade-emitted scores clear the eligibility threshold the same way direct scores do. If the distributor is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, a single trigger credits each peer in the chain with its per-layer score. A collaborator who only ever receives cascade layer-points still has a non-zero score, so they qualify and take a full equal share even though they never personally triggered an event. ## Where this works The Shared Engagement Pool works well when the goal is broad participation rather than peak performance. If you want every contributor to feel they have an equal stake regardless of volume, this structure encourages that. A marketplace with dozens of content creators is one example. If the marketplace pools a percentage of subscription revenue and distributes it to creators monthly, the Shared Engagement Pool ensures every active creator gets something. This can be useful early in a platform's life when you want to attract and retain creators before enough data exists to reward performance proportionally. It also works for flat-fee contributor programs where everyone who participates in a period gets the same stipend. Rather than manually tracking who was active, the distributor handles it automatically through the metric tracking system. ## When to avoid this This structure does not reward high performers. If your goal is to motivate collaborators to outperform each other, or to reward collaborators proportionally to their contributions, use the [Performance Weighted Pool](/documentation/distribution-structures/performance-weighted-pool) or [Top Score Wins](/documentation/distribution-structures/top-score-wins) structure instead. It can also create a free-rider problem. A collaborator who generates one blog post visit gets the same share as one who drives thousands of visits. In programs where contribution levels vary widely, this may not feel fair to your top contributors. ## Comparison with the program-level structure The [program-level Shared Engagement Pool](/documentation/program-structures/shared-engagement-pool) works the same way conceptually, but operates on a per-transaction basis. When a customer converts, the reward for that transaction is split equally among all collaborators who had an engagement with that customer. The distribution-level version operates over a time period instead, splitting the accumulated reward pool among all collaborators who earned any metrics during the period. ## Shared Engagement Pool (Program Structure) Source: https://www.sirenaffiliates.com/documentation/program-structures/shared-engagement-pool A program structure where the reward for a single conversion is split equally among every collaborator who engaged with the customer. The Shared Engagement Pool is a [program](/documentation/general/what-are-programs) structure where the reward for a single conversion is divided equally among every [collaborator](/documentation/general/what-is-a-collaborator) who had any [engagement](/documentation/general/what-is-an-engagement) with the customer. It doesn't matter how high each collaborator's engagement score is. If they engaged at all, they get an equal share. When a customer converts, Siren looks at every collaborator with a non-zero engagement score for that customer, divides the conversion reward by the number of qualifying collaborators, and creates one obligation per collaborator for an equal share. ## How it works If a customer converts on a $1,000 sale with a 10% commission, the reward for that [conversion](/documentation/general/what-is-a-conversion) is $100. If three collaborators each had some engagement with that customer (scores of 500, 200, and 50), all three receive an equal $33.33 share. The collaborator who drove 500 engagement points gets the same payout as the one who only drove 50. ## How engagement scores feed the split This structure uses the engagement score only to decide eligibility. Any non-zero score earns an equal share. Cascade-emitted engagement scores count toward that threshold the same way direct scores do. If the program is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, a single trigger credits one engagement per layer. A collaborator who only ever receives cascade layer-points clears the non-zero threshold and takes a full equal share, even though they never personally engaged the customer. ## Where this works This structure fits programs where multiple touchpoints genuinely contribute to a sale and you want to credit everyone involved without running a contest over who contributed most. It works best when commissions are large enough that splitting them still leaves each collaborator meaningfully paid. The [multi-touch sales attribution](/recipes/multi-touch-sales-attribution) recipe uses this structure to split credit equally among every affiliate who touched a sale. High-ticket sales cycles with multiple experts involved (consultants, content creators, closers) are a natural fit. ## When to avoid this If contribution levels vary widely and your top performers would resent subsidizing minor contributors, use the [Performance Weighted Pool](/documentation/program-structures/performance-weighted-pool) instead. If you want a single winner per conversion, use [Top Score Wins](/documentation/program-structures/top-score-wins). It also doesn't work well for low-margin sales. Splitting a $5 commission four ways leaves each collaborator with $1.25, which usually isn't enough to motivate anyone. ## Comparison with the distribution-level structure The [distribution-level Shared Engagement Pool](/documentation/distribution-structures/shared-engagement-pool) works the same way conceptually but operates over a time period instead of per conversion. The program-level version splits a single conversion reward among engaged collaborators. The distribution-level version splits an accumulated reward pool among every collaborator who earned any metric score during the period. ## Sharing Creatives with Your Collaborators Source: https://www.sirenaffiliates.com/documentation/getting-started/creatives-for-collaborators How to give your affiliates banners, swipe copy, and promotional assets when Siren doesn't have a built-in creative library. The WordPress page pattern. Siren doesn't include a built-in creatives library. There's no upload-banners-and-swipe-copy screen, no "pick a creative" dropdown on the collaborator dashboard, no asset-hosting CDN. If you've come from AffiliateWP, Tapfiliate, or a similar platform, this is going to look like a missing feature. It's deliberate. WordPress already has the tools to host downloadable assets and publish marketing copy, and the result is more flexible than most built-in creatives libraries because you control every aspect of how the assets are presented. This page walks through the pattern. ## The pattern Create a WordPress page (or a private page restricted to logged-in collaborators) that hosts your creatives. Each creative is a section on the page with the asset, suggested usage notes, and a copy-to-clipboard button if the asset is text. Assets can be images uploaded to WordPress media, downloadable files, or text snippets. The page is a normal WordPress page, so you can use the block editor, any page builder, or custom HTML. Whatever your theme supports, your creatives page can use. That's the whole pattern. The rest of this page is how to make that pattern useful. ## A starting template A basic creatives page typically has these sections: - Brand assets (logo files in PNG and SVG, color palette, brand voice notes) - Banners (728x90, 300x250, 160x600, mobile sizes, each displayed with a download link) - Email swipe copy (2-3 example email templates collaborators can adapt) - Social media copy (short captions for Twitter/X, longer ones for LinkedIn, hashtag suggestions) - Product screenshots (marketing-quality screenshots of your product or service) - Approved messaging (things you DO want collaborators to say, and things you DON'T) You don't need all of these. A scrappy affiliate program can ship with just logos and a paragraph of swipe copy. An enterprise partner program might have separate pages for each partner tier with pre-approved, legal-reviewed assets. The structure scales with what you need. One thing worth including that most built-in creatives libraries miss: a section explaining what NOT to say. If your product has FTC disclosure requirements, trademark restrictions, or claims your legal team won't approve, spell them out on the page. Built-in creatives libraries usually just give affiliates assets without context. A WordPress page lets you wrap every asset in as much guidance as you want, which prevents the "an affiliate made wildly inaccurate claims about our product and we only found out in a customer service email" scenario. ## A note on copy-to-clipboard For text assets (swipe copy, social captions, affiliate links), a copy-to-clipboard button is the difference between "collaborator uses the text you gave them" and "collaborator retypes something similar and introduces errors." Any decent page builder has a copy-to-clipboard block. If yours doesn't, a five-line vanilla JavaScript snippet will get you there. Put the text in a styled code block, add the button, done. Collaborators click, paste, and the exact wording you approved ends up in whatever they're posting. This single detail prevents about 80% of "why did an affiliate use the wrong phrase?" problems. ## Restricting access If you want only your collaborators to see the creatives page, you've got two options. The more secure route uses a membership plugin. Set the page's visibility to private and use MemberPress, Paid Memberships Pro, Restrict Content Pro, or a similar plugin to grant access to users in a "collaborators" role. This scales cleanly to many collaborators and prevents unauthorized access even if someone shares the URL. The trade-off is more setup time and another plugin to maintain. The lighter route uses a hidden page with an obscure URL. Create a public page with a hard-to-guess slug (something like `/partners/creatives-x7q/`) and share the link in your collaborator welcome email. This isn't truly private because anyone with the URL can view it, but it's fine for most non-sensitive brand assets. It requires no plugin, no membership infrastructure, and no role-based access control. You can always upgrade to the membership plugin approach later if your threat model changes. The membership plugin approach is what you want if the assets are confidential or legally sensitive, or if you want per-tier creative pages where premium partners see assets standard partners don't. The hidden URL approach is fine for everything else. ## Linking from the collaborator dashboard Add a link to your creatives page from the welcome email collaborators receive when they sign up. (If you haven't gotten as far as filling out that welcome email because you're still looking for collaborators to send it to, [how to find your first affiliates](/blog/how-to-find-your-first-affiliates) is worth a detour.) This is the single most important place to mention it, because collaborators read the welcome email carefully when they're getting set up and ignore it later. If the link isn't in the welcome email, half your collaborators will never find the creatives page and will end up making their own banners from screenshots. The other half will email you asking where the assets are. Today, the cleanest path is the welcome email link and a mention in whatever documentation you give new collaborators. A followup email sent a week after signup ("Did you see our creatives page?") catches the collaborators who missed it the first time and gently nudges the ones who've forgotten. ## Keeping creatives up to date The biggest failure mode for creatives pages is that they go stale. A banner that references last year's branding, an email template that mentions a discontinued feature, a product screenshot showing a five-version-old UI. Collaborators use whatever you give them, so stale assets make their way into the wild and stay there. The fix is to put a "last updated" date at the top of the creatives page and treat it as a quarterly review item. Every three months, walk through each asset and confirm it's still current. Replace anything that references old pricing, old features, or old branding. This is a 20-minute task four times a year and saves you from the situation where an affiliate is running a year-old promotional campaign that contradicts your current messaging. If you ship a major product change (a rebrand, a new feature launch, a pricing update), update the creatives page the same week. Send a followup email to collaborators pointing at the new assets so they know to swap out their existing promotional material. ## For agencies running this for clients If you're an agency running affiliate programs on behalf of multiple clients, build a single template page once and save it as a reusable block or pattern in WordPress. Stamp it onto each new client site, then customize the brand assets for that client. The structure stays the same across clients: same sections, same layout, same restricted-access setup. Only the actual assets change. This turns a creatives page from "build from scratch for every client" into "fill in the blanks for every client," which is usually what you want when you're running five or ten programs at once. You can also build a shared "agency standards" document that tells your team which sections to include for which kinds of clients (a B2B SaaS client probably doesn't need 728x90 banners, a consumer product client almost certainly does). ## Why Siren doesn't have a built-in creatives library Most affiliate plugins ship a creatives system because they're trying to be a complete affiliate management product. They want to be the single place you manage everything: programs, creatives, payouts, reporting, communication, all in one admin screen. That's a defensible product decision, and for some teams it's the right one. Siren's design is different. It's the commission engine, and it leaves anything WordPress already handles well to WordPress. Hosting downloadable assets and writing marketing copy is squarely in WordPress's wheelhouse, so adding a creatives feature would mean rebuilding what WordPress already does, plus maintaining it forever. The trade-off is that the setup is slightly more manual the first time. You'll spend an hour building a creatives page that a built-in library would give you in ten minutes. The upside is that you have complete control over how your creatives page looks and works, and you're not locked into a pre-built layout someone else designed. You also get WordPress's full ecosystem: page builders, block editors, media management, user roles, membership plugins, all the things you already use for the rest of your site. ## Closing note This might change in a future version if customer feedback signals that the manual approach isn't working. For now, the WordPress page pattern is the recommended path, and most operators find it takes about an hour to set up and doesn't come up again after that. If you try the pattern and find a gap that the WordPress-native approach doesn't cover, let us know. The design choice to lean on WordPress is based on the assumption that the existing tools are sufficient. If that assumption is wrong in your situation, that's valuable feedback and it's the kind of thing that could shift what Siren builds next. ## Site Visited Source: https://www.sirenaffiliates.com/documentation/general/site-visited When someone visits your site through a collaborator's referral link, the Site Visited event is triggered. import EventFlow from "@/components/content/EventFlow.astro"; When a visitor arrives at your site through a [collaborator's](/documentation/general/what-is-a-collaborator) referral link, the "Site Visited" event is triggered. This creates an [engagement](/documentation/general/what-is-an-engagement) linking the visitor to the collaborator who owns the link. ## How it's typically used Site Visited is the most common engagement trigger and the one most people think of when they picture an affiliate program. A collaborator shares their link in a blog post, an email, or a social post, and every click creates or refreshes an engagement tied to that collaborator. When the visitor eventually buys something, the program has a record of who sent them. Because it fires on every visit, Site Visited is usually paired with a "newest engagement wins" or "oldest engagement wins" sorter in the [program's](/documentation/general/what-are-programs) settings. That's how the program decides which collaborator gets credit when a visitor has clicked multiple links across the same session or across multiple sessions. ## Where this fits The [basic affiliate program](/recipes/basic-affiliate-program) recipe uses Site Visited as its primary tracking event. It's also the foundation for any program where the collaborator's job is to drive traffic, whether that's a classic affiliate relationship, a paid media partner, or an influencer who sends people through a bio link. ## StudentCompletedLesson Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-system/student-completed-lesson Fires when a student finishes a lesson in a connected LMS. Powers engagement tracking for education-based incentive programs. # StudentCompletedLesson `StudentCompletedLesson` fires when a student finishes a lesson in a connected learning management system. This is how Siren powers education-based incentive programs: course creators and educators can be rewarded when students they referred or are associated with complete lessons, giving program operators a way to incentivize teaching quality and student engagement. The event ID is `student_completed_lesson`, and its fully qualified class is `Siren\LMS\Core\Events\StudentCompletedLesson`. ## What does this event carry? The event carries the `Student` model, the `Lesson` model, an array of `Educator` models representing the instructors associated with the lesson, and an optional `Course` model when the lesson belongs to a structured course. ```php use Siren\LMS\Core\Events\StudentCompletedLesson; public function handle(Event $event): void { $student = $event->getStudent(); $lesson = $event->getLesson(); $educators = $event->getEducators(); $course = $event->getCourse(); // may be null // Educators are the collaborators who can be rewarded // for student completions in their lessons or courses } ``` ## How does this connect to the incentive pipeline? The educator models in the event are the link to Siren's collaborator system. When a student completes a lesson, the system can create engagements and conversions that credit the associated educators, just as a site visit or coupon code credits a traditional affiliate. Programs configured with lesson-completion triggers use this event as their entry point. ## What about course-level completions? When a student finishes an entire course rather than a single lesson, the system fires `StudentCompletedCourse` instead. That event follows the same pattern but represents the broader milestone. Programs can be configured to trigger on either lesson completions, course completions, or both, depending on the granularity the program operator wants. For the full lifecycle of system events and how they connect to the rest of Siren's architecture, see the [System Events overview](/documentation/developer-reference/events-system). ## Submit Collaborator Request Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/submit-request Public-facing endpoint for the collaborator signup form, secured by a signed JWT token. ### Submit Collaborator Request `POST /siren/v1/collaborators/submit-request` Public-facing endpoint used by the collaborator signup form. Accepts a signed JWT (generated by the admin-side Create Signup Form JWT endpoint) that carries program enrollment and approval settings. The collaborator is created or matched based on email, and the signup submission event is broadcast for downstream processing. This endpoint does not require standard authentication. It is secured by the signed JWT token included in the request body. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `fullName` | string | Yes | The applicant's full name | | `nickname` | string | Yes | The applicant's display name | | `email` | string | Yes | The applicant's email address | | `jwt` | string | Yes | Signed JWT containing `programIds`, `statusOnSignup`, and `approveExisting` | ## System and Collaborator Events Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-system Domain events for collaborator registration, LMS integration, and recipe import. # System and Collaborator Events This page covers events that operate outside the core conversion and payment pipelines. These include the collaborator registration flow, LMS integration events, and the recipe import system. The events span several domains: `Siren\Collaborators\Core\Events`, `Siren\LMS\Core\Events`, and `Siren\Recipes\Core\Events`. ## Collaborator registration flow Collaborator events track the lifecycle of a new collaborator joining the system. The flow moves through three stages: a submission is received, the submission is processed, and the collaborator's account becomes ready. [CollaboratorSubmissionReceived](/documentation/developer-reference/events-system/collaborator-submission-received) fires when a new collaborator registration request arrives. It gives listeners the opportunity to modify or enrich the submission before the collaborator record is created. A listener could auto-assign the collaborator to programs based on a referral code or set default field values based on the source. [CollaboratorAccountReady](/documentation/developer-reference/events-system/collaborator-account-ready) fires when a collaborator's account is fully set up and ready to use. This event fires after all submission processing is complete and the account has been activated. It is the signal that the collaborator can now participate in programs and receive attribution. ## LMS events LMS events fire when students complete lessons or courses in a connected learning management system. These events power engagement tracking for education-based incentive programs, where collaborators earn rewards based on student progress. [StudentCompletedLesson](/documentation/developer-reference/events-system/student-completed-lesson) fires when a student finishes a lesson. The educator array carried by the event determines which collaborators receive engagement credit for the completion. A structurally similar `StudentCompletedCourse` event fires for full course completions and typically triggers larger rewards. ## Recipe import The recipe system allows pre-built program configurations to be applied to a Siren installation through a single event. [RecipeApplyRequested](/documentation/developer-reference/events-system/recipe-apply-requested) fires when a recipe should be applied to the current installation. The event is mutable: as handlers process different parts of the recipe, they record the created entities back onto the event so that later handlers can reference them. This allows complex configurations with cross-references between programs, program groups, and distributors to be applied in a single pass. ## Terms for Collaborators Source: https://www.sirenaffiliates.com/documentation/general/collaborator-glossary How Siren's internal terminology translates to the language collaborators (affiliates, creators, instructors) actually use when they're participating in a program. Siren's documentation is mostly written for the person running a program, not for the people earning from it. Internally, we talk about obligations, conversions, and engagements because those words describe the underlying data model accurately. The trouble is that collaborators don't use those words. An affiliate asks "when do I get paid?" not "when will my obligation reach the complete state?" This page has two audiences. If you're a [collaborator](/documentation/general/what-is-a-collaborator) reading Siren-powered documentation or looking at a dashboard, you can use the table below as a translation guide for the words you're seeing. If you're a program owner writing onboarding emails, support responses, or help docs for your collaborators, you can use this page as a list of terms to avoid. The internal vocabulary is precise, but it's also cold and bureaucratic. Collaborators respond better to the words they already use. ## The translation table Each row shows one Siren term and the words collaborators actually use for it. When you're writing to a collaborator, pick from the right column. | Siren term (program-owner side) | What collaborators call it | |---|---| | [Obligation](/documentation/general/what-are-obligations) | Pending reward, earned commission, money owed | | [Fulfillment](/documentation/general/what-is-a-fulfillment) | Payout, payment, payment batch | | [Conversion](/documentation/general/what-is-a-conversion) | Sale credit, earned sale, successful referral | | [Engagement](/documentation/general/what-is-an-engagement) | Click, visit, touch, referral | | [Collaborator](/documentation/general/what-is-a-collaborator) | You, affiliate, creator, partner, instructor, ambassador | | [Program](/documentation/general/what-are-programs) | The deal you're in, your commission structure, your plan | | [Program Group](/documentation/general/what-are-program-groups) | Your tier, the program family you're in | | [Collaborator Group](/documentation/general/what-are-collaborator-groups) | Your team, your roster, the chain you're in | | [Cascade](/documentation/general/what-is-a-cascade) (Upline / Downline) | Team override, tier commission, override on your team's sales | | Approved (status) | Confirmed | | Pending (status) | Awaiting review, pending payout | | Rejected (status) | Cancelled, voided, reversed | | Complete (status) | Paid | | Tracking ID / [Alias](/documentation/resource-reference/aliases) | Your code, your referral ID, your link | | [Opportunity](/documentation/general/what-is-an-opportunity) | Your visitor, your lead | | [Distribution](/documentation/general/what-are-distributors) | Bonus payout, performance bonus, leaderboard prize | | [Coupon code](/documentation/general/coupon-tracking) | Your code, your promo code | A few patterns to watch for when you're writing to collaborators: The words "conversion," "engagement," and "opportunity" all sound like analytics jargon to someone who just wants to know whether they made a sale. When possible, replace them with "sale," "click," or "visit." The word "obligation" is especially cold. It's precise internally because an obligation is a debt on your books that hasn't been paid yet, but to a collaborator it reads as stiff and legalistic. "Pending reward" or "earned commission" lands better. The word "rejected" is technically accurate for both manual rejection and refund-triggered rejection, but it reads like the collaborator did something wrong. If a conversion is rejected because of a refund, say "reversed" or "cancelled" instead. Save "rejected" for cases where you're declining a submission. The words "cascade," "upline," and "downline" are internal terms for how a [collaborator group](/documentation/general/what-are-collaborator-groups) pays peers above or below the person who made the sale. A collaborator who earns from someone else's sale will ask "why did I get paid for a sale I didn't make?" Skip the cascade vocabulary and explain it as the deal they signed up for: they earn an override when someone on their team makes a sale, or a commission on the tier below them. The [cascade concept doc](/documentation/general/what-is-a-cascade) explains the mechanics if you need to describe a specific layout. ## A note on refund language Siren's refund pipeline uses terms like "rejected obligation" and "cancelled conversion." Those are accurate database descriptions, but they read as adversarial when a collaborator sees them in their dashboard. From their side it can feel like "the program owner just took my commission back and marked it rejected like I did something wrong." That's rarely the intent, but the vocabulary makes it sound that way. A friendlier framing for collaborator-facing communication sounds like this: "If a customer returns the order, the matching reward is reversed automatically. Whether that reversed amount gets deducted from your next payout depends on our refund policy, which we'll explain upfront so nothing is a surprise." The important move here is to explain your refund policy in the onboarding email rather than letting collaborators discover it through Siren's internal status changes. If you wait until a refund happens to explain what a "rejected obligation" means, you're having a stressful conversation instead of a routine one. The [how refunds work](/documentation/general/how-refunds-work) doc covers the mechanics of what Siren does automatically, so you know what to describe in your own words. ## Looking ahead The new collaborator experience in an upcoming Siren version will use plain-language terms throughout the collaborator-facing dashboard, so much of this translation layer will eventually become invisible. Until then, this page is a stable bridge. Use it when you're writing onboarding materials, answering support questions, or helping a collaborator make sense of what they're seeing. If you're looking for the canonical definitions of the terms in the left column, every link in the table points to the concept doc for that term in the User Guide. ## The Amount & Currency System Source: https://www.sirenaffiliates.com/documentation/extensions/amount-currency Working with monetary values — Amount objects, cents-as-integers, Currency model, price adapters. # The Amount & Currency System Siren represents all monetary values as integers in the smallest currency unit (cents for USD, pence for GBP, etc.). This avoids the floating-point precision errors that plague financial software. The `Amount` model pairs an integer value with a `Currency` object, and the `FloatToIntPriceAdapter` handles conversions. --- ## What is the Amount model? The `Amount` model (`Siren\Commerce\Models\Amount`) is a value object that pairs an integer value with its currency. ```php class Amount { protected int $value; protected Currency $currency; public function __construct(int $value, Currency $currency) { $this->value = $value; $this->currency = $currency; } public function getValue(): int // e.g., 1999 for $19.99 public function getCurrency(): Currency } ``` `Amount` is a value object — it holds the numeric value and its associated currency, nothing more. It has no setters; create a new instance to represent a different amount. ### Key Design Decision: Integers, Not Floats All monetary values throughout Siren are integers representing the smallest currency unit. For USD, `1999` means $19.99. For JPY (which has no fractional unit), `1999` means 1999 yen. Why integers? ```php // This is wrong — floating point: 0.1 + 0.2 === 0.3 // false in most languages // This is correct — integer cents: 10 + 20 === 30 // always true ``` Floating-point arithmetic produces rounding errors that compound across thousands of transactions. When calculating commissions on affiliate sales, even a fraction-of-a-cent error per transaction becomes real money at scale. --- ## How does Siren represent currencies? The `Currency` model (`Siren\Commerce\Models\Currency`) stores a currency code, display symbol, and symbol position. ```php class Currency implements DataModel, HasSingleStringIdentity { use WithSingleStringIdentity; protected string $symbol; protected string $position; public function __construct( string $id, // ISO 4217 code: 'USD', 'EUR', 'GBP' string $symbol, // Display symbol: '$', '€', '£' string $position = 'before' // 'before' or 'after' ) } ``` | Property | Type | Description | |----------|------|-------------| | `$id` | `string` | ISO 4217 currency code (e.g., `'USD'`) | | `$symbol` | `string` | Display symbol (e.g., `'$'`, `'€'`, `'R$'`) | | `$position` | `string` | Whether symbol appears `'before'` or `'after'` the number | ### Available Currencies Siren registers 23 currencies out of the box via `RegisterCoreCurrencies`: | Code | Symbol | Code | Symbol | Code | Symbol | |------|--------|------|--------|------|--------| | USD | $ | EUR | € | GBP | £ | | JPY | ¥ | CNY | ¥ | AUD | $ | | CAD | $ | INR | ₹ | BRL | R$ | | ZAR | R | SGD | $ | MYR | RM | | THB | ฿ | SEK | kr | CHF | CHF | | NZD | $ | MXN | $ | HKD | $ | | NOK | kr | KRW | ₩ | TRY | ₺ | | RUB | ₽ | PLN | zł | | | Additional currencies can be registered by listening to the `CurrencyRegistryInitiated` event: ```php use Siren\Configs\Core\Events\CurrencyRegistryInitiated; use Siren\Commerce\Models\Currency; // In your initializer's getListeners(): CurrencyRegistryInitiated::class => MyCustomCurrencyRegistrar::class, // In the handler: public function handle(Event $event): void { $event->addCurrency('NGN', fn() => new Currency('NGN', '₦')); } ``` --- ## How do you convert between float prices and integer cents? `FloatToIntPriceAdapter` converts between the float prices that e-commerce platforms use and the integer cents that Siren stores. ```php class FloatToIntPriceAdapter { public function toInt(float $amount): int // 19.99 → 1999 public function toFloat(int $amount): float // 1999 → 19.99 public function toString(Amount $amount, $decimalSeparator = '.', $thousandsSeparator = ','): string } ``` ### toInt — Platform Price to Siren ```php $adapter = new FloatToIntPriceAdapter(); // WooCommerce returns floats: $wcPrice = $order->get_total(); // 49.99 // Convert for Siren: $cents = $adapter->toInt($wcPrice); // 4999 ``` This is used extensively in order-to-transaction-details adapters. Every line item value, shipping total, tax total, and discount must go through `toInt()` before entering the transaction details array. ### toFloat — Siren to Display ```php $displayPrice = $adapter->toFloat(4999); // 49.99 ``` ### toString — Formatted Currency String ```php $amount = new Amount(4999, new Currency('USD', '$')); $formatted = $adapter->toString($amount); // "$49.99" $euroAmount = new Amount(4999, new Currency('EUR', '€', 'after')); $formatted = $adapter->toString($euroAmount, ',', '.'); // "49,99€" ``` The `toString` method respects the currency's `position` property — symbols marked `'after'` are appended, otherwise prepended. --- ## Using Amount in Transaction Details When building the transaction details array for `SaleTriggered`, every `value` field must be in integer cents. Here is the pattern from the WooCommerce adapter: ```php // From OrderToTransactionDetailsAdapter::toArray() $shippingTotal = $this->priceAdapter->toInt($order->get_shipping_total()); if ($shippingTotal > 0) { $result[] = [ 'name' => 'Shipping', 'description' => 'Shipping Fees', 'type' => 'shipping', 'value' => $shippingTotal, // Integer cents 'quantity' => 1, 'units' => $currency, // e.g., 'USD' 'externalId' => null ]; } // For line items, value is per-unit: $total = $this->priceAdapter->toInt($item->get_total()); $result[] = [ 'value' => $total / $item->get_quantity(), // Per-unit price in cents 'quantity' => $item->get_quantity(), // ... ]; ``` The `value` field in transaction details is the per-unit price, not the line total. The system multiplies `value * quantity` internally. Discounts are represented as negative values: ```php $discount = $this->priceAdapter->toInt($order->get_discount_total()); if ($discount > 0) { $result[] = [ 'type' => 'discount', 'value' => $discount * -1, // Negative value // ... ]; } ``` --- ## Amount in Incentive Calculations The incentive system uses `Amount` to represent commission pools — the total reward to distribute for a conversion. ### How does Siren resolve currencies for programs? This service resolves the correct currency for a program's incentive calculations: ```php public function buildCurrency(int $programId): Currency { $program = $this->programs->find($programId); $units = $this->currencyProviderService->getCurrencyFromCode($program->getUnits()); if (!isset($units)) { $units = $this->currencyProviderService->getDefaultCurrency(); } return $units; } ``` Each program has a `units` field (e.g., `'USD'`). The service resolves this to a `Currency` object, falling back to the system default if the program's currency is not found. ### Reward Pool Calculation Pool calculators return `Amount` objects: ```php // StandardRewardPoolCalculationService $pool = $poolCalculator($binding->getProgramId(), $conversion, $transaction); return new Amount($pool, $this->currencyProvider->buildCurrency($binding->getProgramId())); // LeadRewardPoolCalculationService (fixed amount per lead) $pool = (int) $this->incentiveConfigDatastore->getConfig($programId, 'payoutPerLead', 0); return new Amount($pool, $this->currencyProvider->buildCurrency($programId)); ``` The `$pool` value is always an integer in cents. The `Amount` wrapping adds currency context so downstream formatters know how to display it. --- ## How do you access currencies at runtime? The `CurrencyProviderService` is the central service for currency operations. Inject it via the interface `Siren\Commerce\Interfaces\CurrencyProviderService`: ```php interface CurrencyProviderService { public function getCurrencyFromCode(string $currencyCode): ?Currency; public function getDefaultCurrency(): ?Currency; public function setCurrency(Currency $currency); public function getCurrencyIdentifiers(): array; // ['USD', 'EUR', ...] public function getCurrencies(): array; // [Currency, Currency, ...] } ``` The default currency is stored in the config table under group `'commerce'`, key `'currencyCode'`, context `'default'`. It defaults to `'USD'` if not set. --- ## Summary of Conventions Always store monetary values as integers — multiply by 100 when ingesting from platforms, and always use `FloatToIntPriceAdapter::toInt()` rather than casting manually. The `value` field in transaction details is per-unit; the system handles the `value * quantity` multiplication. Discounts should be negative (multiply the amount by -1). Each program can operate in a different currency, so currency is resolved from the program configuration. And `Amount` is a value object — create new instances rather than mutating existing ones. ## The Attribution Pipeline Source: https://www.sirenaffiliates.com/documentation/resource-reference/pipeline-overview End-to-end walkthrough of how a customer interaction becomes a collaborator payout. Covers every stage from opportunity creation through fulfillment. import EventFlow from "@/components/content/EventFlow.astro"; # The Attribution Pipeline When a customer clicks an affiliate link, Siren begins tracking a chain of events that can end with a collaborator receiving a payout. The path from that first click to a deposited commission passes through six distinct stages, each connected only by domain events. No stage calls the next directly. Each one reacts to the event that came before it, which means extensions can hook into any point in the pipeline without modifying the core. This page walks through the full pipeline end-to-end. If you are debugging a specific stage, this is where you find out which events to look at and how data flows between them. If you are building an extension, this is where you learn where your code fits in. ## The full pipeline at a glance Each stage produces one or more events. Each event triggers listeners that advance the pipeline to the next stage. The sections below explain what happens at each stage, which events fire, and how data threads through from start to finish. ## Stage 1: Opportunity creation The pipeline begins when a customer arrives through a referral mechanism. When someone clicks an affiliate link, Siren creates an **opportunity** record that captures the referral context: which link was used, when the visit happened, and what tracking data is available. The opportunity is Siren's way of saying "this customer might convert, and here's who referred them." The `OpportunityTriggered` event carries the opportunity model and the trigger type (link visit, coupon code, manual creation). Engagement trigger strategies listen for this event and decide which collaborators should receive credit claims. If the opportunity is invalid (expired link, duplicate detection, validation failure), `OpportunityInvalidated` fires instead and the pipeline stops here. An opportunity carries an ID that threads through the entire pipeline. Every subsequent event references this ID to trace back to the original customer interaction. ## Stage 2: Engagement tracking Engagements represent a collaborator's credit claim against a specific program. When an opportunity fires, the system evaluates which programs apply and which collaborators are eligible, then creates engagement records. A single opportunity can produce multiple engagements. If a collaborator participates in three programs, the system creates three engagement records, each linking the collaborator to a specific program for this opportunity. Each engagement has a score that program group resolution strategies use later to determine which engagement wins if programs are mutually exclusive. Engagements sit in an active state until a commerce event converts them. They are claims, not credits. The collaborator does not earn anything yet. ## Stage 3: Commerce events Commerce events are the entry points where real business activity enters the pipeline. When an e-commerce platform detects a sale, renewal, refund, or lead, the appropriate extension produces a commerce event. These events carry the financial details that the conversion system needs to calculate what each collaborator earns. `SaleTriggered` is the most common entry point. It carries the opportunity ID (linking back to the referral), transaction details (line items with names, values, and types), the source extension identifier, and optional binding fields for mapping back to external records like WooCommerce order IDs. Extensions do not dispatch commerce events directly. They return event instances from transformer callbacks, and the framework broadcasts them. This keeps extension code cleanly separated from the core attribution logic. ### How commerce events branch The pipeline branches depending on how the customer was attributed and what kind of activity occurred. **Link attribution** follows the standard path. The opportunity was created by a link click, engagements already exist, and the sale event triggers conversion processing against those existing engagements. **Coupon attribution** works differently. `CouponApplied` fires first when the customer enters a coupon code at checkout, creating engagements directly from the coupon ownership. A subsequent `SaleTriggered` event then converts those engagements into credits. **Lead attribution** uses `LeadTriggered` instead of `SaleTriggered`. Leads carry an opportunity ID and source string but no transaction details. The downstream pipeline uses lead-specific incentive types rather than sale-based ones. **Renewals** use `RenewalTriggered`, which traces back to the original purchase transaction so the same collaborator continues earning on recurring revenue. The conversion process mirrors the initial sale flow but reuses the original engagement records instead of looking up a new opportunity. ## Stage 4: Conversion building This is where the pipeline calculates what each collaborator actually earns. The conversion system takes the commerce event, finds all qualifying programs, and builds conversion records. `ConversionInitialized` is the handoff event from commerce to conversions. The `BuildConversions` listener picks it up, retrieves all programs compatible with the conversion type, and delegates to the `ConversionAwardStrategy` for each qualifying program. The strategy evaluates which engagements win (using program group resolution if programs are mutually exclusive), calculates incentive amounts, and creates conversion records. `ConversionsAwarded` fires once per program that produces conversions. Each conversion links to a specific engagement, an optional transaction, and will later link to an obligation. Conversions start in draft status. Conversions advance to approved status through one of two paths. If the transaction's payment clears, `TransactionCompleted` fires and the `ApproveTransactionConversions` listener automatically approves all conversions linked to that transaction. Alternatively, an admin can approve conversions manually through the dashboard. Either way, `ConversionApproved` fires for each approved conversion, which triggers the next stage. If a conversion is rejected (refund, admin decision, fraud), `ConversionRejected` fires and the associated obligations are cleaned up. ### Program groups and mutual exclusivity When programs belong to a program group, only one program per group can award conversions for a given opportunity. `ProgramGroupConversionTriggered` fires to identify the winning program (determined by the group's resolution strategy), and listeners clean up engagements on the losing programs. ### Manual attribution Admins can bypass the normal engagement flow entirely using `ManualAttributionRequested`. This creates conversions for a specific collaborator without requiring an existing engagement, useful for crediting partners retroactively or handling edge cases that the automated pipeline did not capture. ## Stage 5: Obligations and transactions When conversions are awarded, the system creates obligation records that track what the business owes each collaborator. `ObligationIssued` fires when an obligation record is created. Each obligation has a collaborator, a value (in the smallest currency unit), an award type (commission, bonus, etc.), and a status. Obligations start in draft status. When a conversion is approved, the `MarkObligationsAsPending` listener transitions the obligation from draft to pending. Pending obligations are eligible for fulfillment and payout. This two-step status progression (draft then pending) gives admins a review window before obligations become payable. On the transaction side, `TransactionCreateRequested` fires before a transaction is written to the database. This is a mutable event that lets listeners add, remove, or transform transaction details before creation. After the transaction is persisted, `TransactionCreated` fires for logging, analytics, and external system syncing. ## Stage 6: Fulfillment and payout The final stage compiles pending obligations into concrete payments. A fulfillment is a batch operation that groups pending obligations for disbursement. Within a fulfillment, the system creates one payout per collaborator, aggregating all their pending obligations into a single payment amount. `FulfillmentCreated` fires when the batch begins, and `FulfillmentStatusChanged` fires as it moves through its lifecycle (pending, processing, completed). `PayoutCreated` fires for each individual collaborator payment. `PayoutPaid` is the terminal event: the collaborator has been paid. When a payout is marked as paid, the associated obligations transition to fulfilled status and the payout ID is written back to each obligation record. ## How distributions work differently Distributions are a separate pipeline for scheduled, metric-based rewards. Instead of reacting to individual sales, distributions accumulate performance metrics over time and periodically award collaborators based on aggregate performance. `DistributionHeartbeatInitialized` fires on a regular interval. The system queries for distributions whose trigger date has passed, calculates the reward pool, determines each qualifying collaborator's share based on accumulated metrics, and creates allocation records. Each allocation produces an obligation that enters the same fulfillment pipeline as conversion-based obligations. The key difference is timing and basis. The sale pipeline creates obligations in real-time from individual transactions. The distribution pipeline creates obligations on a schedule from aggregated performance data. Both converge at the fulfillment and payout stage. Metrics accumulate continuously between distribution periods. `MetricsTriggered` fires when new metric data is recorded, with different trigger strategies producing metrics from different sources (sales and site visits). See the [Distribution Events](/documentation/developer-reference/events-distributions) reference for details. ## How data threads through the pipeline Understanding which IDs connect the stages is critical for debugging and extension development. The **opportunity ID** is the anchor. It is created in stage 1 and referenced by every subsequent event through the pipeline. If you are debugging why a collaborator did not get credited for a sale, the opportunity ID is where you start. The **engagement ID** connects a collaborator and program to an opportunity. Conversions reference the engagement that triggered them, so the engagement ID tells you which collaborator-program combination earned credit. The **transaction ID** is optional and only exists when the commerce event included financial details (sales and renewals, not leads). Conversions reference the transaction, and approving the transaction approves its conversions. The **obligation ID** is written to both the conversion record and the payout record, making it the link between "what was earned" and "what was paid." ## Where to go from here Each stage has its own detailed event reference with payload documentation, listener details, and code examples: - [Commerce Events](/documentation/developer-reference/events-commerce) for sales, refunds, coupons, renewals, and leads - [Attribution Events](/documentation/developer-reference/events-attribution) for opportunity tracking and engagement creation - [Conversion Events](/documentation/developer-reference/events-conversions) for the conversion lifecycle from initialization through approval - [Payment Events](/documentation/developer-reference/events-payments) for transactions, obligations, fulfillments, and payouts - [Distribution Events](/documentation/developer-reference/events-distributions) for the scheduled reward system - [System Events](/documentation/developer-reference/events-system) for collaborator registration, LMS, and recipes For a general introduction to how events work in Siren (listening patterns, the `HasListeners` interface, direct attachment), see the [Events Introduction](/documentation/developer-reference/events-introduction). ## The Collaborator Dashboard Source: https://www.sirenaffiliates.com/documentation/getting-started/the-collaborator-dashboard A demo of what collaborators see when they log into their account. import RelatedDocs from "@/components/content/RelatedDocs.astro"; import Transcript from "@/components/media/Transcript.astro"; ## What collaborators see when they log in When a [collaborator](/documentation/general/what-is-a-collaborator) logs into your WordPress site, they get a dedicated dashboard with everything they need to track their participation. It's designed to be self-service so you don't field questions about payment status or referral stats. ## Earnings and payment history The top of the dashboard shows a rewards summary split into paid, unpaid, and rejected. Below that, a detailed history lists every [obligation](/documentation/general/what-are-obligations) on their account with amount, status, and payment date. Pending [fulfillments](/documentation/general/what-is-a-fulfillment) show up here too, so collaborators can see when a payment is on the way without asking you. ## Engagement stats by program The engagements section breaks activity down by time period (today, this week, this month) and by [program](/documentation/general/what-are-programs). If a collaborator is enrolled in multiple programs, each program's [engagements](/documentation/general/what-is-an-engagement) appear separately so they can see which ones are generating the most activity. ## Coupons and referral links Any coupon codes assigned to the collaborator appear in the Coupons section, ready to share. When a customer uses one at checkout, Siren tracks it as an engagement and ties the [conversion](/documentation/general/what-is-a-conversion) back to the code's owner. The dashboard also has a URL generator. Collaborators paste in any page on your site and get back a referral link with their unique code already attached. There's also a "Get Referral Link" button in the WordPress toolbar, so a logged-in collaborator browsing your store can grab a referral link for the page they're already on with one click. ## Customizing the portal's appearance The standalone Collaborator Portal (Essentials and above) automatically picks up your site's colors via WordPress Global Styles. You can also set specific background, text, and accent colors plus a custom sidebar logo through the block editor or shortcode. See [Customizing the Collaborator Portal](/documentation/getting-started/customizing-the-collaborator-portal) for the full setup. ## Collaborators are real WordPress users This is one of the things that sets Siren apart from most affiliate plugins. Collaborators aren't entries in a secondary database table. They're real WordPress users, which means they can have any role and capability on top of being a collaborator. A single user might be an author publishing blog posts, a course creator building content in LifterLMS or LearnDash, and an affiliate promoting your store, all at once. The WordPress profile, menus, and capabilities work as expected, and the collaborator dashboard slots into that existing experience instead of replacing it. That matters, because Siren's most powerful use cases are usually people who contribute to your site in more than one way. The collaborator dashboard shows rewards (paid, unpaid, rejected), a detailed obligation history, engagement counts broken down by program and time period, any assigned coupon codes, and a referral link generator. There's also a "Get Referral Link" button in the WordPress toolbar that copies a link for whichever page the collaborator is currently viewing. The thing that sets Siren apart here is that collaborators are real WordPress users. They can also be authors, course creators, or shop managers in the same account, with all the WordPress capabilities you'd expect, instead of being stuck in a separate affiliate-only table. ## The Integration Class Source: https://www.sirenaffiliates.com/documentation/extensions/integration-class Anatomy of Integration.php — required interfaces, lifecycle methods, admin service patterns, and the processing order. # The Integration Class Every Siren extension is anchored by a single `Integration` class. This class tells Siren what your extension does, when it should activate, what domain events it bridges, and what platform features it supports. It is the single entry point the framework uses to discover and bootstrap your extension. ## Required Interfaces An integration class implements a specific set of interfaces. Here is the minimum set every commerce extension uses: ```php use PHPNomad\Di\Interfaces\CanSetContainer; use PHPNomad\Di\Traits\HasSettableContainer; use PHPNomad\Events\Interfaces\HasEventBindings; use PHPNomad\Loader\Interfaces\HasLoadCondition; use PHPNomad\Loader\Interfaces\Loadable; use Siren\Extensions\Core\Interfaces\Extension; class Integration implements Extension, HasEventBindings, HasLoadCondition, CanSetContainer, Loadable { use HasSettableContainer; // ... } ``` Each interface serves a distinct purpose. `Extension` is Siren's own interface that declares this class as a Siren extension. It extends `Module`, requiring `getId()` and `getRootPath()`, and adds `getName()`, `getDescription()`, `canActivate()`, `getIsActive()`, and `getSupports()`. The remaining interfaces come from the [PHPNomad framework](https://phpnomad.com/). [`HasEventBindings`](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-binding) declares that this class maps platform hooks to domain events via `getEventBindings()`. [`HasListeners`](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners) is optional and declares that this class registers handlers for domain events via `getListeners()`. [`HasLoadCondition`](https://phpnomad.com/core-concepts/bootstrapping/creating-and-managing-initializers/) provides `shouldLoad()`. This is a gate that controls whether the extension bootstraps at all. `CanSetContainer` allows the framework to inject the DI container (use the `HasSettableContainer` trait to satisfy it). [`Loadable`](https://phpnomad.com/core-concepts/bootstrapping/creating-and-managing-initializers/) provides `load()`. This is the final setup method called after the framework decides to bootstrap the extension. ### When should I add listeners? Most commerce integrations (WooCommerce, EDD, NorthCommerce, LifterLMS) only implement `HasEventBindings` because they only need to bridge platform hooks into domain events. Add `HasListeners` when your extension also needs to *react* to domain events during bootstrap. The Gravity Forms integration is the canonical example: ```php // From GravityForms Integration — uses both HasEventBindings AND HasListeners class Integration implements Extension, HasEventBindings, HasListeners, HasLoadCondition, CanSetContainer, Loadable { // ... public function getListeners(): array { return [ Ready::class => [ InitializeGravityFormsAddon::class ] ]; } } ``` This registers `InitializeGravityFormsAddon` to run when the framework fires `Ready`, allowing the GF add-on to be registered at exactly the right time. ## Interface Processing Order The framework processes these interfaces in a specific order, and the order matters: The DI container is injected first via `CanSetContainer` — this must happen before anything else because event binding closures resolve services from the container. Next the framework checks `shouldLoad()` via `HasLoadCondition`, and if it returns `false`, processing stops entirely. Then `getEventBindings()` runs, binding platform hooks to domain event dispatchers. After that, `getListeners()` registers domain event handlers. Finally, `load()` runs for any imperative setup like admin UI initialization and platform-specific hooks. This order guarantees that by the time `getEventBindings()` runs, `$this->container` is available. And by the time `load()` runs, all event bindings and listeners are already wired. ## DI container essentials The container auto-resolves constructor dependencies. If your class asks for a `LoggerStrategy` and a `CollaboratorDatastore`, the container provides them automatically. You only need to register [class definitions](https://phpnomad.com/core-concepts/dependency-injection/) when you are providing a new concrete implementation for an interface. Common services available for injection: | Interface | What it provides | |-----------|-----------------| | `CollaboratorDatastore` | Query and manage collaborators | | `ConfigDatastore` | Read and write configuration values | | `LoggerStrategy` | Log messages and exceptions | | `EventStrategy` | Attach/detach event listeners programmatically | | `ExtensionRegistryService` | Query active extensions and their features | If your extension adds a concrete class that implements an interface other code resolves through the container, register it via `getClassDefinitions()` on any class implementing `HasClassDefinitions`: ```php public function getClassDefinitions(): array { return [ // Key = concrete class, Value = interface MyConcreteService::class => MyServiceInterface::class, ]; } ``` Every entry must be a key-value pair. A bare value like `SomeClass::class` without a key causes the container to try instantiating a class called `"0"`, which fails with a confusing error. If your class does not implement an interface that other code depends on, skip registration entirely. The container auto-resolves concrete classes. For the full [dependency injection documentation](https://phpnomad.com/core-concepts/dependency-injection/), see the PHPNomad framework docs. ## Metadata Methods ### Extension ID A short, unique string identifier for this extension. Used as a module ID for template resolution and as a source identifier in domain events. ```php public static function getId(): string { return 'wc'; // WooCommerce return 'edd'; // Easy Digital Downloads return 'nc'; // NorthCommerce return 'llms'; // LifterLMS return 'gf'; // Gravity Forms } ``` This ID appears throughout the system — in mapping external types (`wc_order`, `edd_order`), in event sources, and in template path prefixes (`wc::module/template`). ### Name and description Human-readable metadata displayed in the admin UI: ```php public function getName(): string { return 'WooCommerce'; } public function getDescription(): string { return 'Makes it possible to award engagements for actions within WooCommerce'; } ``` ### Root path The filesystem root of this extension module, used for locating templates and resources: ```php public function getRootPath(): string { return SIREN_WOOCOMMERCE_ROOT; } ``` Each extension defines a root path constant (e.g., `SIREN_WOOCOMMERCE_ROOT`, `SIREN_EDD_ROOT`). ## How does Siren detect whether the target plugin is installed? `canActivate()` answers: "Is the target plugin installed and available?" This is typically a `class_exists()` check against the target plugin's main class: ```php // WooCommerce public function canActivate(): bool { return class_exists('WooCommerce'); } // EDD public function canActivate(): bool { return class_exists('Easy_Digital_Downloads'); } // LifterLMS public function canActivate(): bool { return class_exists('LifterLMS'); } // Gravity Forms — checks multiple possible entry points public function canActivate(): bool { return class_exists('GFForms') || class_exists('GFCommon'); } ``` `canActivate()` is part of the `Extension` interface and is used by the admin UI to show whether an extension *could* be activated. It does not control loading — that is `shouldLoad()`'s job. ## How do I conditionally control whether my extension loads? `shouldLoad()` controls whether the framework actually bootstraps this extension. In most cases it simply delegates to `canActivate()`: ```php public function shouldLoad(): bool { return $this->canActivate(); } ``` But `shouldLoad()` can add extra conditions beyond plugin detection. NorthCommerce requires a minimum Siren version: ```php // NorthCommerce — requires Siren 1.3+ public function shouldLoad(): bool { return $this->canActivate() && version_compare( $this->container->get(VersionProvider::class)->getVersion(), '1.3', '>=' ); } ``` The distinction matters: `canActivate()` tells the admin "this extension is available," while `shouldLoad()` tells the framework "actually load it." An extension might be activatable but not loadable (e.g., wrong Siren version, missing configuration, license check). ## How do I declare which features my extension supports? `getSupports()` returns an array of `Features` enum values declaring what platform capabilities this extension provides: ```php use Siren\Extensions\Core\Enums\Features; // WooCommerce public function getSupports(): array { $supports = [ Features::Coupons, Features::ManualOrdering ]; if (class_exists('WC_Subscriptions')) { $supports[] = Features::Renewals; } return $supports; } ``` Available feature constants: | Constant | Value | Meaning | |----------|-------|---------| | `Features::Coupons` | `'coupons'` | Extension can track coupon usage | | `Features::ManualOrdering` | `'manual_ordering'` | Extension supports manual order creation | | `Features::Renewals` | `'renewals'` | Extension can track subscription renewals | | `Features::Courses` | `'courses'` | Extension can track course completions | | `Features::Lessons` | `'lessons'` | Extension can track lesson completions | | `Features::Forms` | `'forms'` | Extension can track form submissions | | `Features::Posts` | `'posts'` | Extension can track post interactions | Note how WooCommerce conditionally adds `Features::Renewals` based on whether WooCommerce Subscriptions is installed. Feature support can be dynamic. ## How do I map hooks to domain events? This is the core of what an integration does. See [Event Bindings and Transformers](/documentation/extensions/event-bindings-and-transformers) for full coverage. ## How do I register domain event handlers? When implemented, `getListeners()` returns a map of domain event classes to handler classes: ```php public function getListeners(): array { return [ Ready::class => [ InitializeGravityFormsAddon::class ] ]; } ``` The format is `EventClass::class => [HandlerClass::class, ...]`. Each handler must implement `PHPNomad\Events\Interfaces\CanHandle`. Handlers are resolved from the DI container, so their constructor dependencies are auto-wired. ## What goes in the final setup method? `load()` runs after all bindings and listeners are registered. It is where you set up WordPress-specific admin UI, filters, and other side effects: ```php public function load(): void { $this->isActive = true; add_action('plugins_loaded', fn() => $this->container->get(CouponAdminService::class)->init()); add_action('plugins_loaded', fn() => $this->container->get(ProductAdminService::class)->init()); } ``` Always set `$isActive = true` first — `getIsActive()` reports whether the load method has run. Defer admin service initialization to `plugins_loaded` to ensure the target plugin's APIs are available. And always use `$this->container->get()` to resolve services rather than instantiating directly. WooCommerce's `load()` shows a more complex example that also handles collaborator access to the WooCommerce admin: ```php public function load(): void { $this->isActive = true; add_action('plugins_loaded', fn() => $this->container->get(CouponAdminService::class)->init()); add_action('plugins_loaded', fn() => $this->container->get(ProductAdminService::class)->init()); add_action('plugins_loaded', function () { $user = wp_get_current_user(); try { $collaborator = Collaborators::getCollaboratorFromUserId($user->ID); } catch (RecordNotFoundException $e) { return; } catch (DatastoreErrorException $e) { Logger::logException($e); return; } if ($collaborator) { add_filter('woocommerce_prevent_admin_access', '__return_false'); } }); } ``` ### The admin service pattern Admin services are plain PHP classes that add platform-specific UI panels for mapping Siren entities (collaborators, programs) to external entities (products, coupons). They follow a consistent structure: receive datastores via constructor injection, register WordPress hooks in an `init()` method, and handle save/render in protected methods. ```php class ProductAdminService { protected MappingDatastore $mappings; protected CollaboratorDatastore $collaborators; public function __construct(MappingDatastore $mappings, CollaboratorDatastore $collaborators) { $this->mappings = $mappings; $this->collaborators = $collaborators; } public function init(): void { add_action('save_post_product', [$this, 'handleSave']); // Register meta boxes, admin columns, etc. } } ``` The `load()` method resolves admin services from the container and wraps `init()` in a `plugins_loaded` callback to ensure the target plugin's classes are available. This is the standard pattern — the examples above show it in action. Admin services commonly use the [mapping system](/documentation/extensions/mappings) to store relationships between external entities and Siren entities. See the mappings guide for the clear-then-recreate save pattern that admin services use internally. ## Complete Skeleton Here is a minimal integration class for a hypothetical "AcmeShop" plugin: ```php canActivate(); } public function getSupports(): array { return [Features::Coupons, Features::ManualOrdering]; } public function getIsActive(): bool { return $this->isActive; } public function getEventBindings(): array { $saleCallback = fn($orderId) => $this->container->get( SaleTransformer::class )->getSaleTriggeredEvent($orderId); return [ SaleTriggered::class => [ ['action' => 'acme_order_created', 'transformer' => $saleCallback], ], ]; } public function load(): void { $this->isActive = true; add_action('plugins_loaded', fn() => $this->container->get( ProductAdminService::class )->init()); } } ``` ## The Mapping System Source: https://www.sirenaffiliates.com/documentation/extensions/mappings Using Mapping to bridge Siren internal IDs with external system IDs. # The Mapping System Mappings are Siren's mechanism for bridging internal IDs with external system IDs. When WooCommerce creates an order, EDD processes a payment, or NorthCommerce sells a product, the mapping system records the relationship between Siren's internal identifiers (such as [transactions](/documentation/resource-reference/transactions) and [collaborators](/documentation/resource-reference/collaborators)) and the external platform's identifiers. This enables duplicate detection, reverse lookups, and clean separation between Siren's domain and third-party systems. ## The Mapping Model The `Mapping` model lives at `lib/Mappings/Core/Models/Mapping.php`: ```php namespace Siren\Mappings\Core\Models; use PHPNomad\Datastore\Interfaces\DataModel; final class Mapping implements DataModel { protected int $localId; protected $externalId; // string|int protected string $localType; protected string $externalType; public function __construct( int $localId, $externalId, string $localType, string $externalType ) { /* ... */ } public function getLocalId(): int { return $this->localId; } public function getExternalId() { return $this->externalId; } public function getLocalType(): string { return $this->localType; } public function getExternalType(): string { return $this->externalType; } public function getIdentity(): array { return [ 'localId' => $this->getLocalId(), 'externalId' => $this->getExternalId(), 'localType' => $this->getLocalType(), 'externalType' => $this->getExternalType(), ]; } } ``` ### The Four Fields | Field | Type | Description | Example | |-------|------|-------------|---------| | `localId` | `int` | Siren's internal ID | Collaborator ID `42`, Transaction ID `15` | | `externalId` | `string\|int` | The external system's ID | WooCommerce order ID `1087`, product ID `55` | | `localType` | `string` | What kind of Siren entity | `'collaborator'`, `'transaction'` | | `externalType` | `string` | What kind of external entity | `'wc_order'`, `'wc_product'`, `'nc_product'` | The combination of all four fields forms the compound primary key. There are no auto-increment IDs on the mappings table. ## Common Use Cases ### Product-to-Collaborator Mappings When a store owner assigns collaborators to products in the admin UI, each assignment is stored as a mapping: | localId | externalId | localType | externalType | |---------|------------|-----------|--------------| | 42 | 55 | collaborator | wc_product | | 42 | 12 | collaborator | nc_product | | 7 | 55 | collaborator | wc_product | ### Order-to-Transaction Mappings When a WooCommerce order generates a Siren transaction, the relationship is recorded: | localId | externalId | localType | externalType | |---------|------------|-----------|--------------| | 15 | 1087 | transaction | wc_order | ### Naming conventions for types The `localType` is always a Siren domain entity in lowercase (`collaborator`, `transaction`, `program`). The `externalType` uses a platform prefix plus entity name (`wc_product`, `wc_order`, `nc_product`, `edd_payment`). ## How do I query and manage mappings? The `MappingDatastore` interface (`lib/Mappings/Core/Datastores/Mapping/Interfaces/MappingDatastore.php`) provides purpose-built query methods: ```php interface MappingDatastore extends Datastore, DatastoreHasWhere, DatastoreHasCounts { /** * Find a mapping by all four fields (exact match). * @throws RecordNotFoundException */ public function find( int $localId, string $localType, int $externalId, string $externalType ): Mapping; /** * Find a mapping by its Siren-side identity. * @throws RecordNotFoundException */ public function getByLocalId( int $localId, string $localType, string $externalType ): Mapping; /** * Find a mapping by its external-system identity. * @throws RecordNotFoundException */ public function getByExternalId( $externalId, string $externalType, string $localType ): Mapping; /** Delete a specific mapping by all four fields. */ public function delete( int $localId, string $localType, int $externalId, string $externalType ): void; /** Delete all mappings for a given Siren entity. */ public function deleteMappingsForLocalId(int $localId, string $localType): void; /** Delete all mappings for a given external entity. */ public function deleteMappingsForExternalId(int $externalId, string $externalType): void; } ``` ### How Queries Work Internally The concrete `MappingDatastore` delegates to the datastore handler using `andWhere()`: ```php public function getByLocalId(int $localId, string $localType, string $externalType): Mapping { $models = $this->datastoreHandler->andWhere([ ['column' => 'localId', 'operator' => '=', 'value' => $localId], ['column' => 'localType', 'operator' => '=', 'value' => $localType], ['column' => 'externalType', 'operator' => '=', 'value' => $externalType] ]); if (empty($models)) { throw new RecordNotFoundException('The local mapping could not be found.'); } return Arr::get($models, 0); } ``` ## Creating Mappings Use the standard datastore `create()` method: ```php $this->mappings->create([ 'externalId' => $product->get_id(), 'localId' => $collaborator->getId(), 'localType' => 'collaborator', 'externalType' => 'wc_product' ]); ``` ## Updating Mappings: The Clear-Then-Recreate Pattern Siren does not update mappings in place. Instead, it deletes existing mappings and creates new ones. This is the standard pattern used in every admin save handler: ```php // From WooCommerce/Services/ProductAdminService::handleSave() protected function handleSave($product) { try { if (isset($_POST['siren-product-collaborators']) && $product instanceof WC_Product) { $collaborators = $this->collaborators->findMultiple( Arr::cast(array_map('absint', wp_unslash($_POST['siren-product-collaborators'])), 'int') ); // Step 1: Clear existing mappings for this product $this->mappings->deleteWhere([ ['column' => 'externalId', 'operator' => '=', 'value' => $product->get_id()], ['column' => 'externalType', 'operator' => '=', 'value' => 'wc_product'], ['column' => 'localType', 'operator' => '=', 'value' => 'collaborator'] ]); // Step 2: Create fresh mappings foreach ($collaborators as $collaborator) { $this->mappings->create([ 'externalId' => $product->get_id(), 'localId' => $collaborator->getId(), 'localType' => 'collaborator', 'externalType' => 'wc_product' ]); } } } catch (DatastoreErrorException $e) { $this->logger->logException($e); } } ``` NorthCommerce follows the same pattern, using `nc_product` as the `externalType`: ```php // From NorthCommerce/Services/ProductAdminService::handleSave() $this->mappings->deleteWhere([ ['column' => 'externalId', 'operator' => '=', 'value' => $productId], ['column' => 'externalType', 'operator' => '=', 'value' => 'nc_product'], ['column' => 'localType', 'operator' => '=', 'value' => 'collaborator'], ]); foreach ($collaborators as $collaborator) { $this->mappings->create([ 'externalId' => $productId, 'localId' => $collaborator->getId(), 'localType' => 'collaborator', 'externalType' => 'nc_product', ]); } ``` ## Duplicate Prevention Using Mappings The most important use of mappings is preventing duplicate transaction processing. When a WooCommerce order fires sale events (which can happen multiple times due to status transitions), the transformer checks for an existing mapping before creating a new transaction: ```php // From WooCommerce/Services/TransactionTransformerService.php protected function orderHasTransaction(int $orderId) { try { $this->mappings->getByExternalId($orderId, 'wc_order', 'transaction'); return true; } catch (RecordNotFoundException $e) { // No mapping found -- this order hasn't been processed yet } catch (DatastoreErrorException $e) { $this->logger->logException($e); } return false; } public function getSaleTriggeredEvent($orderId): ?SaleTriggered { $orderId = WC()->order_factory->get_order_id($orderId); if (!$orderId) { return null; } $opportunity = $this->locateOpportunity($orderId); if (!$opportunity) { return null; } // Guard: don't create duplicate transactions if ($this->orderHasTransaction($orderId)) { return null; } $transactionDetails = $this->detailsAdapter->toArray($orderId); if (empty($transactionDetails)) { return null; } return new SaleTriggered( $opportunity->getId(), $transactionDetails, 'wc', $orderId, 'wc_order' ); } ``` The flow: 1. WooCommerce fires `woocommerce_order_status_processing`. 2. The transformer calls `orderHasTransaction($orderId)`. 3. If a mapping exists (`wc_order:1087 -> transaction`), return `null`. The order is a duplicate. 4. If no mapping, proceed with creating the `SaleTriggered` event. 5. Downstream, when the transaction is created, a mapping is written. ## Cleaning Up Mappings on Deletion NorthCommerce handles product deletion by removing all related mappings: ```php protected function handleDelete(int $productId): void { try { if (!empty($productId)) { $this->mappings->deleteWhere([ ['column' => 'externalId', 'operator' => '=', 'value' => $productId], ['column' => 'externalType', 'operator' => '=', 'value' => 'nc_product'], ['column' => 'localType', 'operator' => '=', 'value' => 'collaborator'], ]); } } catch (DatastoreErrorException $e) { $this->logger->logException($e); } } ``` You can also use the convenience methods on `MappingDatastore`: ```php // Delete all mappings for a Siren collaborator $this->mappings->deleteMappingsForLocalId($collaboratorId, 'collaborator'); // Delete all mappings for a WooCommerce product $this->mappings->deleteMappingsForExternalId($productId, 'wc_product'); ``` ## The Mappings Table Schema The mappings table uses a compound primary key with no auto-increment ID: ```php // From MappingsTable public function getColumns(): array { return $this->mergeTenantColumns([ new Column('localId', 'BIGINT', null, 'NOT NULL'), new Column('externalId', 'CHAR', [32], 'NOT NULL'), new Column('localType', 'VARCHAR', [255], 'NOT NULL'), new Column('externalType', 'VARCHAR', [255], 'NOT NULL'), ]); } public function getIndices(): array { $identity = ['localId', 'externalId', 'localType', 'externalType']; return $this->mergeTenantIndices([ new Index($identity, '', 'PRIMARY KEY'), new Index($identity, 'identity_index', 'INDEX') ]); } ``` Note that `externalId` is `CHAR(32)`, not `BIGINT`. This accommodates external systems that use non-numeric identifiers (UUIDs, slugs, etc.). ## How do I access the mapping datastore in my code? Inject `MappingDatastore` via the interface: ```php use Siren\Mappings\Core\Datastores\Mapping\Interfaces\MappingDatastore; class MyService { protected MappingDatastore $mappings; public function __construct(MappingDatastore $mappings) { $this->mappings = $mappings; } } ``` The DI container resolves the concrete implementation automatically. The mapping domain's `Initializer` handles the bindings: ```php // From Mappings/Service/Initializer.php public function getClassDefinitions(): array { return [ MappingDatabaseDatastoreHandler::class => MappingDatastoreHandler::class, MappingDatastore::class => MappingDatastoreInterface::class, ]; } ``` ## Checklist for Using Mappings in a New Extension 1. Inject `MappingDatastore` into your service or admin service. 2. Define consistent `localType` and `externalType` strings for your integration. 3. Use clear-then-recreate on save operations to keep mappings in sync. 4. Use `getByExternalId()` for duplicate detection before creating records. 5. Clean up mappings when external entities are deleted. 6. Wrap all mapping operations in try/catch for `DatastoreErrorException`. ## Top Score Wins (Distribution Structure) Source: https://www.sirenaffiliates.com/documentation/distribution-structures/top-score-wins A distribution structure where the entire reward pool goes to the collaborator with the highest metric score during the period. Top Score Wins is a distribution structure where the entire reward pool goes to the single collaborator with the highest metric score when the distribution triggers. Nobody else receives anything for that period. This is the most competitive distribution structure. When a distribution triggers on schedule, Siren compares metric scores across all collaborators, identifies the one with the highest total, and creates a single [obligation](/documentation/general/what-are-obligations) for the full reward pool amount. Every other collaborator's metrics are reset along with the winner's, and the next period starts fresh. ## How the reward pool is calculated The reward pool for a distribution is based on a percentage of revenue collected since the last distribution. You configure this percentage when setting up the [distributor](/documentation/general/what-are-distributors). For example, if you set the pool to 5% and your site earned $100,000 since the last distribution, the reward pool for that period is $5,000. With Top Score Wins, that entire $5,000 goes to a single collaborator. If three collaborators have scores of 850, 720, and 430, only the one with 850 receives the payout. ## How metric scores work in this structure Each distributor is configured with [tracking events](/documentation/general/what-are-distributors#tracking-events-and-metric-values) that define what it measures. As these events happen during the distribution period, Siren accumulates a metric score for each collaborator. Each event type has a configurable point value. In Top Score Wins, these scores determine who takes the entire pool. The point values you assign to different tracking events shape what "winning" looks like. If a distributor tracks both site visits (1 point each) and product sales (10 points each), a collaborator who sold 20 products would score 200 while one who drove 150 site visits would score 150. The seller wins. Adjusting these values lets you define what the competition actually measures. Cascade-emitted scores count toward the winning total the same way direct scores do. If the distributor is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, a single trigger credits each peer in the chain with its per-layer score. A collaborator who sits high in the chain accumulates layer-points across every trigger below them, so they can out-score the direct contributors and win the entire pool without personally triggering anything. ## Where this works Top Score Wins is built for competitive bonus programs where you want a clear winner. The [monthly sales bonus](/recipes/monthly-sales-bonus) recipe uses this structure to award a bonus to the top-performing affiliate each month. The [milestone rewards program](/recipes/milestone-rewards-program) applies the same approach to reward the collaborator who hits the highest performance milestones in a period. This structure works well when you want to: - Run a monthly or quarterly competition with a single prize - Motivate your top tier of collaborators to push harder - Create a leaderboard dynamic where the reward is meaningful because it isn't shared Sales team bonuses are a natural fit. If you have a team of salespeople and want to award a monthly bonus to whoever generated the most revenue, Top Score Wins handles the tracking, comparison, and payout automatically. ## When to avoid this This structure only pays one person. If most of your collaborators have no realistic chance of winning, the incentive loses its motivating power. In programs with a wide range of contributor sizes, the same collaborator may win every period, which discourages everyone else. If you want to reward all contributors based on their relative performance, use the [Performance Weighted Pool](/documentation/distribution-structures/performance-weighted-pool). If you want equal shares for everyone who participated, use the [Shared Engagement Pool](/documentation/distribution-structures/shared-engagement-pool). Also be cautious about gaming. If the tracked events are easy to inflate artificially (for example, site visits that a collaborator could generate themselves), a winner-take-all structure amplifies that incentive. Make sure the events you track are meaningful and difficult to manipulate. A cascade compounds this risk for whoever sits high in the chain, because they accumulate per-layer points from every trigger beneath them, so weigh the layer values carefully if a cascade-bound distributor uses this structure. ## Comparison with the program-level structure The [program-level Top Score Wins](/documentation/program-structures/top-score-wins) works the same way conceptually, but operates on a per-transaction basis. When a customer converts, the full reward goes to the collaborator with the highest engagement score for that customer. The distribution-level version operates over a time period instead, comparing metric scores accumulated across all activity during the period and awarding the entire reward pool to the top scorer. ## Top Score Wins (Program Structure) Source: https://www.sirenaffiliates.com/documentation/program-structures/top-score-wins A program structure where the full reward for a conversion goes to the single collaborator with the highest engagement score for that customer. Top Score Wins is a [program](/documentation/general/what-are-programs) structure where the entire reward for a single conversion goes to the one [collaborator](/documentation/general/what-is-a-collaborator) with the highest [engagement](/documentation/general/what-is-an-engagement) score for that customer. Nobody else receives anything for that conversion. When a customer converts, Siren compares engagement scores across every collaborator who engaged with that customer, identifies the highest, and creates a single obligation for the full reward amount. ## How it works If a customer converts on a $200 sale with a 25% commission, the reward for that [conversion](/documentation/general/what-is-a-conversion) is $50. If three collaborators have engagement scores of 850, 720, and 430 for that customer, the collaborator with 850 receives the entire $50. The other two get nothing. The point values assigned to different engagement events shape what "winning" means. If tutorial completions are worth 10 points and link clicks are worth 1, a collaborator whose content gets customers to finish a tutorial will almost always outrank one who just drove clicks. ## How engagement scores feed the comparison Cascade-emitted engagement scores count toward the comparison the same way direct scores do. If the program is bound to a [linear-chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) collaborator group with an [Upline](/documentation/calculation-strategies/upline-cascade) or [Downline](/documentation/calculation-strategies/downline-cascade) cascade calc, a single trigger credits one engagement per layer at the per-layer score you configured. A collaborator who sits at a high-paying layer can accumulate enough cascade points to out-score direct contributors and win the entire reward without personally engaging the customer. Weigh this when you set per-layer values, because winner-take-all gives whoever sits high in the chain a structural edge. ## Where this works Top Score Wins suits programs where you want to reward the single most impactful contribution to each sale, not a collaborative split. It works well when you've identified specific high-value behaviors (in-depth content, hands-on demos, closing conversations) and you want collaborators to compete to own them. The [top performer affiliate program](/recipes/top-performer-affiliate-program) recipe uses this structure to award the full commission to the affiliate with the strongest engagement on each sale. ## When to avoid this If every touchpoint in your sales cycle matters and nobody should walk away empty-handed, use the [Performance Weighted Pool](/documentation/program-structures/performance-weighted-pool) instead. It still rewards top performers more but doesn't shut out everyone else. Be cautious if your engagement events are easy to game. Winner-take-all amplifies the incentive to inflate scores artificially, so make sure the events you track reflect real contribution and can't be triggered cheaply. ## Comparison with the distribution-level structure The [distribution-level Top Score Wins](/documentation/distribution-structures/top-score-wins) works the same way conceptually but operates over a time period instead of per conversion. The program-level version awards each conversion's reward to the top-scoring collaborator for that customer. The distribution-level version awards an accumulated reward pool to whichever collaborator had the highest metric score across all activity during the period. ## Transaction Filtering Source: https://www.sirenaffiliates.com/documentation/general/line-item-filters How to control which parts of a transaction count toward commissions: line items, discounts, fees, shipping, taxes, and product-level filters. When a customer places an order, the resulting [transaction](/documentation/general/what-are-transactions) contains several types of financial data: the products purchased, any discounts applied, fees, shipping costs, and taxes. By default, Siren does not include any of this data in the commission calculation. You choose which parts count by enabling transaction compilers on your [programs](/documentation/general/what-are-programs) and [distributors](/documentation/general/what-are-distributors). Transaction compilers are the building blocks that tell Siren how to read a transaction. Each compiler handles one type of data, and you can enable as many as you need for a given program or distributor. ## Transaction compilers There are five built-in compilers. Each one adds a specific piece of the transaction to the commission calculation. ### Line Items Includes the actual products purchased. This is the most common compiler and the foundation of most commission calculations. When a customer buys three products, the line items compiler pulls in each product's price so the incentive structure can calculate the reward. When line items are enabled, additional filtering options appear. These let you narrow which products count toward the commission. See the line item filters section below for details. ### Discounts Includes discount amounts in the calculation. When enabled, Siren subtracts any applied coupons or discounts from the total before calculating the commission. If a $120 product has a $20 coupon applied, the commission is calculated on $100. If discounts are not enabled, the commission would be calculated on the pre-discount amount. This is almost always something you want enabled. Paying commission on revenue you didn't actually collect is rarely the intent. ### Fees Includes fees like WooCommerce signup fees or subscription setup fees. Whether to enable this depends on your situation. If you charge a one-time setup fee on top of a subscription, you may want that fee included in the commission base. If the fee is a pass-through cost that doesn't represent real revenue, you probably don't. ### Shipping Includes shipping costs in the commission calculation. This is rarely enabled. Shipping is typically a pass-through cost, and most businesses don't want to pay commission on it. ### Taxes Includes tax amounts in the commission calculation. Like shipping, this is almost never enabled. You don't want to pay a percentage commission on the tax you owe the government. ## Line item filters When the Line Items compiler is enabled, four additional filters appear. These narrow which products in the transaction are eligible for the commission. If no filters are set, all line items are included. ### Category filter Restricts commissions to products in specific product categories. You provide a list of category IDs, and only items that belong to those categories are included. A course platform might use this to pay commissions only on products in the "courses" category. Physical merchandise, free downloads, and other product types in the same store would be excluded even if they appear in the same order. ### SKU filter Restricts commissions to products matching specific SKUs. You provide a list of SKUs, and only items with a matching SKU are included. This is useful for fine-grained control. If you're running a promotion where affiliates only earn on a handful of premium products, list those SKUs and everything else is automatically excluded. ### Product type filter Restricts commissions based on whether an item is a one-time product or a subscription. You can choose to include products, subscriptions, or both. A common use is separating subscription commissions from one-time purchase commissions. One program pays a percentage on subscription sign-ups. A different program pays a flat fee per one-time product. The product type filter makes that separation possible without duplicating your product catalog. ### Collaborator owned filter Restricts commissions to products that the collaborator created or owns. A line item only counts if the collaborator tied to the conversion is listed as an owner of that product. This is designed for royalty programs. On a marketplace where multiple creators sell their own products, this filter ensures each creator only earns royalties on their own work. A creator who wrote Course A earns when Course A sells, but not when Course B sells, even if both appear in the same order. ## How filters combine When you enable more than one filter, they work together as a combined requirement. A line item must pass every active filter to be included. If any single filter excludes it, the item is excluded. For example, setting a category filter for "courses" and enabling the collaborator owned filter means a product must be in the "courses" category AND be owned by the collaborator. A course owned by someone else is excluded. A non-course product owned by the collaborator is also excluded. This AND behavior means adding more filters always narrows the set of eligible products. If nothing is matching, check whether your filters are too restrictive in combination. ## Where filtering is configured Transaction compilers and line item filters are configured in two places. On a [program](/documentation/general/what-are-programs), compilers control which parts of the transaction count when a conversion happens. If a customer buys three items but only one passes the line item filters, the commission is calculated against that one item alone. On a [distributor](/documentation/general/what-are-distributors), compilers control which transactions and line items contribute to the distribution's reward pool over the tracking period. The same compilers and filters are available in both contexts. > **For developers:** Transaction compilers are an extension point. See the [transaction compilers extension guide](/documentation/extensions/transaction-compilers) for how to build your own compiler or filter. ## TransactionCompleted Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-commerce/transaction-completed Fires when an order reaches its final approved state, typically after a payment gateway confirms the charge. # TransactionCompleted `TransactionCompleted` fires when an order reaches its final approved state, typically after a payment gateway confirms the charge. This event exists separately from `SaleTriggered` because many e-commerce platforms create orders before payment actually clears. The sale event captures intent; the transaction-completed event captures confirmation. The event ID is `transaction_completed`, and its fully qualified class is `Siren\Commerce\Events\TransactionCompleted`. ## What does this event carry? The event carries the full `Transaction` model. Unlike `SaleTriggered`, which passes raw transaction details as an array, this event works with the already-persisted transaction record that was created during the sale initialization phase. ```php use Siren\Commerce\Events\TransactionCompleted; $event = new TransactionCompleted($transaction); ``` ## How does the pipeline react? When this event fires, `ApproveTransactionConversions` marks the associated conversions as approved. This is the step that advances obligations from draft to pending status, making them eligible for payout. Without this event, conversions remain in a provisional state. For the full lifecycle of commerce events and how they connect to the rest of the attribution pipeline, see the [Commerce Events overview](/documentation/developer-reference/events-commerce). ## TransactionCreated Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/transaction-created Fires after a transaction has been successfully persisted to the database. A read-only notification event. # TransactionCreated Once a transaction and its details have been written to the database, Siren fires `TransactionCreated` to notify the rest of the system. This is a read-only event. By the time it fires, the transaction record is committed and its details are final. The event is identified as `transaction_created` and lives in `Siren\Transactions\Core\Events\TransactionCreated`. ## What does this event carry? The event carries the created `Transaction` model, accessible through `getTransaction()`. The model contains the transaction's ID, status, and all associated details as they were persisted. ```php use Siren\Transactions\Core\Events\TransactionCreated; public function handle(Event $event): void { $transaction = $event->getTransaction(); $id = $transaction->getId(); $status = $transaction->getStatus(); } ``` ## When would you use it? Listeners subscribe to this event for side effects that depend on a committed transaction. Logging, syncing to external systems, and triggering analytics updates are common use cases. Because the transaction is already persisted, there is no risk of acting on data that might still change. If you need to modify the transaction before it is saved, subscribe to [TransactionCreateRequested](/documentation/developer-reference/events-payments/transaction-create-requested) instead. See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## TransactionCreateRequested Source: https://www.sirenaffiliates.com/documentation/developer-reference/events-payments/transaction-create-requested A mutable event that fires before a transaction is persisted. Listeners can modify transaction details before creation. # TransactionCreateRequested Before a transaction is written to the database, Siren gives listeners a chance to inspect and modify the data. `TransactionCreateRequested` is a mutable event that fires during this window, carrying the `transactionDetails` array that will become the transaction's line items. Listeners can add entries, remove them, or transform values before anything is persisted. The event is identified as `transaction_create_requested` and lives in `Siren\Transactions\Core\Events\TransactionCreateRequested`. ## What does this event carry? The event provides a `transactionDetails` array through `getTransactionDetails()` and an `addTransactionDetail` method for appending new entries. It also carries optional `bindingId` and `bindingDataType` fields that link the transaction to an external platform record. Because this event is mutable, any changes a listener makes to the transaction details will propagate to all downstream processing. If multiple listeners modify the same event, they run in the order they were registered, so earlier listeners shape what later listeners see. ## When would you use it? This is the extension point for customizing how transaction data is assembled. A listener might inject additional line items, adjust pricing based on custom business rules, or filter out details that should not factor into commission calculations. ```php use Siren\Transactions\Core\Events\TransactionCreateRequested; public function handle(Event $event): void { $details = $event->getTransactionDetails(); $event->addTransactionDetail([ 'name' => 'Processing Fee', 'description' => 'Custom processing fee', 'value' => 500, 'type' => 'fee', 'quantity' => 1, ]); } ``` Once the event's listeners have all run, the system persists the transaction using the final state of the details array and fires [TransactionCreated](/documentation/developer-reference/events-payments/transaction-created) as a read-only confirmation. See the [Payment Events overview](/documentation/developer-reference/events-payments) for how this event fits into the full payment pipeline. ## Transactions Source: https://www.sirenaffiliates.com/documentation/resource-reference/transactions Financial transaction records in Siren — data model, three-layer PHP structure, status lifecycle, REST API, and PHP data access. import CodeTabs from "@/components/content/CodeTabs.astro"; # Transactions A transaction in Siren represents a commerce event — typically an order, payment, or other monetary exchange coming from an integrated platform such as WooCommerce, EDD, NorthCommerce, or LifterLMS. Transactions sit upstream of [conversions](/documentation/resource-reference/conversions) in the attribution pipeline: when a transaction is attributed to a [collaborator](/documentation/resource-reference/collaborators), it produces an [engagement](/documentation/resource-reference/engagements) and conversion, which in turn triggers an [obligation](/documentation/resource-reference/obligations) recording what is owed. Transactions are created automatically by platform integrations when orders come in, or manually through the API. Each transaction carries one or more detail line items that describe the individual products, credits, or debits that make up the total. In PHP, transactions are organized into three layers. The **transaction** itself is the header record, linked to a conversion. **Transaction details** are the line items within that header — individual products, shipping charges, tax amounts, discounts, and fees. **Transaction detail attributes** are key-value metadata attached to individual line items, such as the product's category list, SKU, or the collaborators who "own" that product. ## The transaction object | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Status | `status` | `getStatus()` | string | Current status: `complete`, `cancelled`, or `refunded` | | Created | `dateCreated` | `getCreatedDate()` | datetime | When the record was created | Transactions are created with a status of `complete` by default. The status field exists to support transitions like refunds and cancellations. ### The TransactionDetail object (line items) Each transaction contains one or more detail line items describing the individual components of the commerce event. | Field | REST name | PHP getter | Type | Description | |---|---|---|---|---| | ID | `id` | `getId()` | integer | Unique identifier | | Transaction | `transactionId` | `getTransactionId()` | integer | The parent transaction this detail belongs to | | Name | `name` | `getName()` | string | Display name (e.g., a product name) | | Description | `description` | `getDescription()` | string | Longer description of the line item | | Type | `type` | `getType()` | string | Type identifier (see detail types below) | | Value | `value` | `getValue()` | integer (REST) / Amount (PHP) | Per-unit value in the smallest currency unit (e.g., cents) | | Quantity | `quantity` | `getQuantity()` | integer | Number of units | | Units | `units` | `getUnits()` | string | Currency code (e.g., `USD`) | In PHP, the `value` field is an `Amount` object that pairs the integer value with its currency. Call `$detail->getValue()->getValue()` to get the raw integer (in cents), and `$detail->getValue()->getCurrency()` to get the `Currency` model. See [The Amount & Currency System](/documentation/extensions/amount-currency) for details on working with monetary values. The `value` represents the per-unit price, not the line total. Siren calculates line totals internally by multiplying `value * quantity`. #### Detail types | Type | Description | |---|---| | `product` | A product line item (the main purchasable goods) | | `subscription` | A subscription product line item | | `shipping` | Shipping charges | | `tax` | Tax amounts | | `discount` | Discount amounts (stored as negative values) | | `fee` | Additional fees (payment processing, handling, etc.) | These types correspond directly to the built-in [transaction compilers](/documentation/extensions/transaction-compilers) that control which detail types are included in commission calculations. A program configured with the `includeLineItems` compiler will include `product` and `subscription` types; the `includeTaxes` compiler includes `tax` types, and so on. See [Line Item Filters](/documentation/general/line-item-filters) for a user-level overview of how these filters are configured through the admin UI. ### The TransactionDetailAttribute object (line item metadata) Transaction detail attributes are key-value pairs attached to individual line items. They store metadata that the core transaction detail model doesn't capture. | Field | PHP getter | Type | Description | |---|---|---|---| | Transaction Detail ID | `getTransactionDetailId()` | integer | The transaction detail this attribute belongs to | | Key | `getKey()` | string | The attribute key (e.g., `sku`, `categories`, `collaborators`) | | Value | `getValue()` | mixed | The attribute value (stored as a string; arrays and objects are JSON-encoded) | Attributes use a compound primary key of `transactionDetailId` + `key`, so each line item can have at most one value per key. The identity is accessible through `getIdentity()` which returns `['transactionDetailId' => ..., 'key' => ...]`. #### Common attribute keys Siren's e-commerce integrations populate these attributes on product line items: | Key | Value format | Description | |---|---|---| | `categories` | JSON array of strings | The product's category names or IDs | | `sku` | string | The product's SKU | | `collaborators` | JSON array of ints | Collaborator IDs associated with ("owning") this product | | `externalId` | string | The product ID in the external platform (WooCommerce product ID, EDD download ID, etc.) | ### The TransactionSource object (REST only) Transaction sources describe the integrations that produce transactions. Each source is registered dynamically based on which platform integrations are active. | Field | Type | Description | |---|---|---| | `id` | string | Unique source type identifier (e.g., `wc_order`) | | `name` | string | Human-readable display name (e.g., `WooCommerce`) | ### Extended fields (REST only, via `?fields=`) These fields are resolved dynamically by the REST API field resolver system and must be explicitly requested. | Field | Type | Description | |---|---|---| | `details` | object[] | Array of transaction detail objects | | `detailCount` | integer | Number of detail line items on the transaction | | `totalValue` | integer | Sum of all detail values, in the smallest currency unit (e.g., cents) | | `totalQuantity` | integer | Sum of all detail quantities | | `currency` | string or null | ISO 4217 currency code from the first detail line item, or null if no details exist | | `source` | string or null | External source type identifier from the mapping system (e.g., `wc_order`) | | `sourceId` | string or null | External source record ID from the mapping system | ## Status lifecycle | Status | Description | |---|---| | `complete` | Default state. The transaction represents a successful commerce event. | | `cancelled` | The transaction has been voided. Downstream conversions linked to this transaction are also cancelled via the CancelRefundedTransactions listener. | | `refunded` | The transaction has been refunded. Triggers the same downstream cancellation as cancelled. | Unlike conversions and obligations, transactions do not have a soft-delete status. The `delete` bulk action permanently removes records. ## Accessing transaction data ```bash # List transactions with detail counts curl -X GET "https://your-site.com/wp-json/siren/v1/transactions?fields=id,status,dateCreated,detailCount,totalValue,currency" \ -H "Authorization: Bearer YOUR_TOKEN" # Get a single transaction with full details curl -X GET "https://your-site.com/wp-json/siren/v1/transactions/15?fields=id,status,details,totalValue,source,sourceId" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ```php use Siren\Transactions\Core\Datastores\Transaction\Interfaces\TransactionDatastore; class MyService { protected TransactionDatastore $transactions; public function __construct(TransactionDatastore $transactions) { $this->transactions = $transactions; } public function getTransaction(int $id): \Siren\Transactions\Core\Models\Transaction { return $this->transactions->getById($id); } } ``` ```php use Siren\Transactions\Core\Facades\Transactions; $transaction = Transactions::getById(42); ``` > Siren is built on an [event-driven architecture](https://phpnomad.com/core-concepts/bootstrapping/initializers/event-listeners). Transaction records are created as side effects of the normal sale pipeline. The `TransactionCreateService` handles creation, fires `TransactionCreateRequested` (allowing listeners to modify or reject the details), and then fires `TransactionCreated` on success. If you find yourself creating transactions manually, consider whether firing a `SaleTriggered` event would be more correct. ## PHP domain methods ### Transaction header methods #### Looking up a transaction by conversion `getByConversionId(int $conversionId)` retrieves the transaction linked to a specific conversion. Each conversion maps to exactly one transaction. Throws `RecordNotFoundException` if no transaction exists for that conversion. ```php use Siren\Transactions\Core\Datastores\Transaction\Interfaces\TransactionDatastore; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; class ConversionInspector { protected TransactionDatastore $transactions; public function __construct(TransactionDatastore $transactions) { $this->transactions = $transactions; } public function getTransactionForConversion(int $conversionId): ?\Siren\Transactions\Core\Models\Transaction { try { return $this->transactions->getByConversionId($conversionId); } catch (RecordNotFoundException $e) { return null; } } } ``` ```php use Siren\Transactions\Core\Facades\Transactions; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $transaction = Transactions::getByConversionId($conversionId); echo 'Transaction #' . $transaction->getId() . ' — status: ' . $transaction->getStatus(); } catch (RecordNotFoundException $e) { echo 'No transaction found for this conversion.'; } ``` ### Transaction detail methods The transaction detail datastore manages individual line items within a transaction. ```php use Siren\Transactions\Core\Datastores\TransactionDetail\Interfaces\TransactionDetailDatastore; class MyService { protected TransactionDetailDatastore $details; public function __construct(TransactionDetailDatastore $details) { $this->details = $details; } } ``` ```php use Siren\Transactions\Core\Facades\TransactionDetails; $detail = TransactionDetails::getById(99); ``` #### Fetching details for a transaction `getDetailsForTransactionId(int $transactionId)` retrieves all line items belonging to a transaction. Returns an array of `TransactionDetail` models. Throws `DatastoreErrorException` on failure. ```php use Siren\Transactions\Core\Datastores\Transaction\Interfaces\TransactionDatastore; use Siren\Transactions\Core\Datastores\TransactionDetail\Interfaces\TransactionDetailDatastore; class OrderSummary { protected TransactionDatastore $transactions; protected TransactionDetailDatastore $details; public function __construct( TransactionDatastore $transactions, TransactionDetailDatastore $details ) { $this->transactions = $transactions; $this->details = $details; } public function summarize(int $conversionId): array { $transaction = $this->transactions->getByConversionId($conversionId); $details = $this->details->getDetailsForTransactionId($transaction->getId()); $summary = []; foreach ($details as $detail) { $summary[] = [ 'name' => $detail->getName(), 'type' => $detail->getType(), 'unitPrice' => $detail->getValue()->getValue(), 'quantity' => $detail->getQuantity(), 'currency' => $detail->getValue()->getCurrency()->getId(), ]; } return $summary; } } ``` ```php use Siren\Transactions\Core\Facades\Transactions; use Siren\Transactions\Core\Facades\TransactionDetails; $transaction = Transactions::getByConversionId($conversionId); $details = TransactionDetails::getDetailsForTransactionId($transaction->getId()); foreach ($details as $detail) { $cents = $detail->getValue()->getValue(); $currency = $detail->getValue()->getCurrency()->getId(); echo $detail->getName() . ': ' . $cents . ' ' . $currency . ' x' . $detail->getQuantity() . ' (' . $detail->getType() . ')' . PHP_EOL; } ``` #### Filtering details by type Transaction details can be filtered by type using the standard `andWhere` method. This is useful when you need only the product lines or only the adjustments (shipping, tax, discount, fee) for a given transaction. ```php use Siren\Transactions\Core\Facades\TransactionDetails; // Get only the product and subscription line items for a transaction $lineItems = TransactionDetails::andWhere([ ['column' => 'transactionId', 'operator' => '=', 'value' => $transactionId], ['column' => 'type', 'operator' => 'IN', 'value' => ['product', 'subscription']], ]); // Get only the adjustments (everything except product lines) $adjustments = TransactionDetails::andWhere([ ['column' => 'transactionId', 'operator' => '=', 'value' => $transactionId], ['column' => 'type', 'operator' => 'IN', 'value' => ['shipping', 'tax', 'discount', 'fee']], ]); ``` ### Transaction detail attribute methods The transaction detail attribute datastore manages key-value metadata on individual line items. ```php use Siren\Transactions\Core\Datastores\TransactionDetailAttribute\Interfaces\TransactionDetailAttributeDatastore; class MyService { protected TransactionDetailAttributeDatastore $attributes; public function __construct(TransactionDetailAttributeDatastore $attributes) { $this->attributes = $attributes; } } ``` ```php use Siren\Transactions\Core\Facades\TransactionDetailAttributes; $sku = TransactionDetailAttributes::getAttributeValue($detailId, 'sku'); ``` #### Getting an attribute value with a default `getAttributeValue(int $transactionDetailId, string $key, $default = null)` retrieves the value for a specific key on a detail, returning the `$default` if the attribute doesn't exist. ```php // Get the SKU, defaulting to 'unknown' if not set $sku = $this->attributes->getAttributeValue($detailId, 'sku', 'unknown'); // Get categories as a PHP array $categories = json_decode( $this->attributes->getAttributeValue($detailId, 'categories', '[]'), true ); ``` ```php use Siren\Transactions\Core\Facades\TransactionDetailAttributes; $sku = TransactionDetailAttributes::getAttributeValue($detailId, 'sku', 'unknown'); $categories = json_decode( TransactionDetailAttributes::getAttributeValue($detailId, 'categories', '[]'), true ); ``` #### Getting the full attribute model `getAttribute(int $transactionDetailId, string $key)` retrieves the full `TransactionDetailAttribute` model for a specific key. Unlike `getAttributeValue`, this throws `RecordNotFoundException` if the attribute doesn't exist, and returns the complete model rather than just the value. ```php use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $attribute = $this->attributes->getAttribute($detailId, 'sku'); echo 'Key: ' . $attribute->getKey() . ', Value: ' . $attribute->getValue(); } catch (RecordNotFoundException $e) { // No SKU attribute on this detail } ``` ```php use Siren\Transactions\Core\Facades\TransactionDetailAttributes; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $attribute = TransactionDetailAttributes::getAttribute($detailId, 'sku'); echo 'Key: ' . $attribute->getKey() . ', Value: ' . $attribute->getValue(); } catch (RecordNotFoundException $e) { // No SKU attribute on this detail } ``` #### Checking product ownership The `collaborators` attribute on product line items powers the "must be owner" feature in the `includeLineItems` transaction compiler. This example checks whether a collaborator is listed as an owner of a specific product line. ```php use Siren\Transactions\Core\Facades\TransactionDetailAttributes; $collaboratorsJson = TransactionDetailAttributes::getAttributeValue( $detailId, 'collaborators', 'null' ); $owners = json_decode($collaboratorsJson, true); if (is_array($owners) && in_array($collaboratorId, $owners)) { echo 'This collaborator owns this product.'; } else { echo 'This collaborator does not own this product.'; } ``` ### Building a full transaction breakdown This example retrieves a transaction for a conversion and builds a complete financial breakdown, including metadata from detail attributes. ```php use Siren\Transactions\Core\Facades\Transactions; use Siren\Transactions\Core\Facades\TransactionDetails; use Siren\Transactions\Core\Facades\TransactionDetailAttributes; use PHPNomad\Datastore\Exceptions\RecordNotFoundException; try { $transaction = Transactions::getByConversionId($conversionId); } catch (RecordNotFoundException $e) { echo 'No transaction for this conversion.'; return; } $details = TransactionDetails::getDetailsForTransactionId($transaction->getId()); $totalCents = 0; foreach ($details as $detail) { $lineCents = $detail->getValue()->getValue() * $detail->getQuantity(); $totalCents += $lineCents; echo $detail->getName() . ' (' . $detail->getType() . '): ' . $lineCents . ' ' . $detail->getValue()->getCurrency()->getId() . PHP_EOL; // Show SKU for product lines if ($detail->getType() === 'product') { $sku = TransactionDetailAttributes::getAttributeValue( $detail->getId(), 'sku', '(no SKU)' ); echo ' SKU: ' . $sku . PHP_EOL; } } echo 'Total: ' . $totalCents . ' cents' . PHP_EOL; ``` ## Relationships - **[Conversion](/documentation/resource-reference/conversions)** (downstream). When a transaction is attributed to a collaborator (either automatically through an engagement or manually via the credit-collaborator endpoint), it produces a conversion. The conversion references the transaction via `transactionId`. - **[Engagement](/documentation/resource-reference/engagements)** (downstream, via attribution). Manual attribution through the credit-collaborator endpoint creates an engagement for the [collaborator](/documentation/resource-reference/collaborators), which in turn produces the conversion. - **[Obligation](/documentation/resource-reference/obligations)** (downstream, via conversion). An approved conversion triggers an obligation recording what is owed to the collaborator. The transaction's value flows through the conversion into the obligation calculation. - **Transaction Details** (children). Each transaction contains one or more detail line items. Details are embedded in the transaction response when the `details` field is requested. - **Mappings** (external link). Transactions originating from platform integrations are linked to their source records via the mapping system, surfaced through the `source` and `sourceId` extended fields. ## REST endpoints See the individual endpoint pages in the sidebar for full request and response details, or browse the [all REST endpoints](/documentation/resource-reference/all-rest-endpoints) reference. ## Update Collaborator Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborators/update Update an existing collaborator's fields and program enrollments. ### Update Collaborator `PUT /siren/v1/collaborators/{id}` Updates an existing collaborator. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). Access is granted to administrators and to the collaborator themselves (owner-bound access). **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `fullName` | string | No | Updated full name | | `nickname` | string | No | Updated display name | | `email` | string | No | Updated email address (must be unique within the organization) | | `status` | string | No | Updated status: `active`, `inactive`, `pending`, or `rejected` | | `programs` | integer[] | No | Complete set of program IDs. Replaces all current enrollments. | When `programs` is provided, the collaborator's current program enrollments are fully replaced. The response includes the updated programs list so the caller does not need a follow-up fetch. **Example Request:** ```json { "status": "inactive", "programs": [1] } ``` **Example Response:** ```json { "id": 7, "fullName": "Jane Smith", "nickname": "Jane", "email": "jane@example.com", "status": "inactive", "createdDate": "2026-04-08T12:00:00Z", "modifiedDate": "2026-04-08T14:30:00Z", "programs": [ { "id": 1, "name": "Standard Affiliate" } ] } ``` **Events:** Broadcasts `CollaboratorActionEvent` (action: Update) after success. ## Update Collaborator Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/collaborator-groups/update Updates an existing collaborator group. Only provided fields are changed. # Update Collaborator Group `PUT /siren/v1/collaborator-groups/{id}` Updates an existing collaborator group. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). Requires authentication and the update capability on the `CollaboratorGroup` resource. Body fields are all optional, and only the keys you send are applied. Member changes do not go through this endpoint. Use [Set members](/documentation/resource-reference/collaborator-groups/set-members) instead. Switching a group's structure does not migrate per-member metadata. Resolvers tolerate unknown keys, so existing member rows keep their data and each resolver reads only the keys it recognizes. Because the metadata does not migrate, members carry no structural position for the new structure until you set one. Switching a populated group to `parentChild`, for example, leaves every member without a parent, so each one reads as a root of the tree until you assign parents through [Set members](/documentation/resource-reference/collaborator-groups/set-members). Switching to `linearChain` is the same class of change: members carry no `position`, so the resolver treats them all as position 0, leaving their order among one another undefined until you set positions. For the full workflow of changing structure on a live program, see [Operating a cascade program](/documentation/getting-started/operating-a-cascade-program). The `structure` value is not checked against the installed resolvers at write time, so an unregistered id is stored as-is and only surfaces as a problem when a calculation or listing tries to resolve it. Use [List structure resolvers](/documentation/resource-reference/collaborator-groups/structures) to get the valid ids for the current install. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | No | Updated display name | | `description` | string | No | Updated description | | `structure` | string | No | Updated structure resolver id: `flat`, `linearChain`, or `parentChild` | **Example Request:** ```json { "name": "Inside Sales", "description": "Renamed", "structure": "parentChild" } ``` **Example Response:** ```json { "id": 12, "name": "Inside Sales", "description": "Renamed", "structure": "parentChild" } ``` Returns 200 with the updated group. The response uses the same adapter shape as [Create collaborator group](/documentation/resource-reference/collaborator-groups/create), so it also includes `dateCreated` and `dateModified` alongside the four fields shown above. An empty body, or a body that sets every field to its current value, is a valid no-op that returns 200 with the current record and broadcasts no events. A description-only change persists and returns 200 but broadcasts no event, since only a name change or a structure change emits one. **Events:** Renaming the group broadcasts `CollaboratorGroupRenamed`, and changing the structure broadcasts `CollaboratorGroupStructureChanged`, for downstream listeners. Each fires only when that field's value actually changes. **Error Responses:** - `404`. No collaborator group found with that ID. - `500`. Database error. ## Update Conversion Source: https://www.sirenaffiliates.com/documentation/resource-reference/conversions/update Update an existing conversion record's fields and status. ### Update Conversion `PUT /siren/v1/conversions/{id}` Updates an existing conversion. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `engagementId` | integer | No | Updated engagement ID (must exist) | | `type` | string | No | Updated conversion type | | `status` | string | No | Updated status: `pending`, `approved`, `rejected`, or `expired` | | `transactionId` | integer | No | Updated transaction ID | | `obligationId` | integer | No | Updated obligation ID | #### Example Request ```json { "status": "approved" } ``` #### Events Broadcasts `ConversionActionEvent` (action: Update) after success. ## Update Distributor Source: https://www.sirenaffiliates.com/documentation/resource-reference/distributors/update Updates an existing distributor. Only provided fields are changed. # Update Distributor `PUT /siren/v1/distributors/{id}` Updates an existing distributor. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `id` | integer | Yes | Distributor ID (must match the URL parameter) | | `name` | string | No | Updated name | | `description` | string | No | Updated description | | `distributionResolver` | string | No | Updated incentive resolver (must be a registered resolver) | | `distributionPoolResolver` | string | No | Updated pool resolver | | `status` | string | No | Updated status: `active` or `inactive` | | `units` | string | No | Updated currency identifier (must be a registered currency) | | `schedule` | string[] | No | Updated distribution schedule | **Example Request:** ```json { "id": 2, "name": "Bi-Weekly Commission", "schedule": ["first day of next month", "+14 days"] } ``` **Example Response:** Returns the full updated distributor record with a `200` status. **Events:** Broadcasts `DistributorActionEvent` (action: Update) after success. ## Update Fulfillment Source: https://www.sirenaffiliates.com/documentation/resource-reference/fulfillments/update Updates a fulfillment record, primarily to advance it through its status lifecycle. ### Update Fulfillment `PUT /siren/v1/fulfillments/{id}` Updates a fulfillment record. Primarily used to advance the fulfillment through its status lifecycle. #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `status` | string | No | New status: `pending`, `processing`, `complete`, or `failed` | #### Example ```json { "status": "processing" } ``` A successful update broadcasts a `FulfillmentStatusChanged` event, which downstream listeners can use to trigger payment processing or notifications. ## Update Obligation Source: https://www.sirenaffiliates.com/documentation/resource-reference/obligations/update Updates an existing obligation record. Only provided fields are changed. ### Update Obligation `PUT /siren/v1/obligations/{id}` Updates an existing obligation. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). #### Request Body | Field | Type | Required | Description | |---|---|---|---| | `collaboratorId` | integer | No | Updated collaborator ID (must exist) | | `status` | string | No | Updated status: `pending`, `fulfilled`, or `cancelled` | | `awardType` | string | No | Updated award type | | `value` | integer | No | Updated value | | `payoutId` | integer | No | Updated payout ID | #### Middleware `RecordExistsMiddleware` verifies the obligation exists before the update proceeds. `CollaboratorAliasResolverMiddleware` resolves collaborator aliases. #### Example Request ```json { "status": "fulfilled", "payoutId": 7 } ``` #### Events Broadcasts `ObligationActionEvent` (action: Update) after success. ## Update Program Source: https://www.sirenaffiliates.com/documentation/resource-reference/programs/update Updates an existing program. Only provided fields are changed. # Update Program `PUT /siren/v1/programs/{id}` Updates an existing program. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). Returns the full updated record in the response body. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | No | Updated display name | | `description` | string | No | Updated description | | `incentiveType` | string | No | Must be a registered incentive type | | `incentiveResolverType` | string | No | Must be a registered incentive resolver type | | `units` | string | No | Must be a registered currency identifier | | `status` | string | No | Updated status: `active`, `inactive`, `draft`, or `deleted` | | `engagementTypes` | object | No | Updated engagement type configuration | **Example Request:** ```json { "status": "inactive", "description": "Program paused for Q2." } ``` **Example Response:** ```json { "id": 1, "name": "Standard Affiliate Program", "description": "Program paused for Q2.", "incentiveType": "commission", "incentiveResolverType": "percentage", "status": "inactive", "units": "USD", "dateCreated": "2026-04-08T12:00:00Z", "dateModified": "2026-04-08T14:30:00Z" } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. **Events:** Broadcasts `ProgramActionEvent` (action: Update) after success. ## Update Program Group Source: https://www.sirenaffiliates.com/documentation/resource-reference/program-groups/update Updates an existing program group. Only provided fields are changed. # Update Program Group `PUT /siren/v1/program-groups/{id}` Updates an existing program group. Only provided fields are changed. The record must exist (enforced by `RecordExistsMiddleware`). When the `programs` array is provided, it replaces the full set of program associations for this group. Existing associations are removed and new ones are created from the provided IDs. Programs already assigned to another group are silently skipped. Omitting `programs` from the request leaves existing associations untouched. **Request Body:** | Field | Type | Required | Description | |---|---|---|---| | `name` | string | No | Updated display name | | `description` | string | No | Updated description | | `sorter` | string | No | Updated sorting strategy: `oldestBindingWins` or `newestBindingWins` | | `programs` | integer[] | No | Replacement set of program IDs. All IDs must reference existing programs. | **Example Request:** ```json { "name": "Seasonal Promotions 2026", "programs": [3, 7, 15] } ``` **Example Response:** ```json { "id": 2, "name": "Seasonal Promotions 2026", "description": "Holiday and seasonal bonus programs that override the default commission.", "sorter": "newestBindingWins" } ``` **Error Responses:** - `404`. No record found with that ID. - `500`. Database error. ## Upgrading to Plus and Pro Source: https://www.sirenaffiliates.com/documentation/migration/upgrading-to-plus-and-pro What collaborator groups and cascades add at each tier, why your existing setup keeps working, and how to start using groups and cascades after you upgrade. # Upgrading to Plus and Pro This guide is for sites already running Siren that want to move up a tier to adopt [collaborator groups](/documentation/general/what-are-collaborator-groups) and [cascades](/documentation/general/what-is-a-cascade). It explains what each tier adds, confirms that your current configuration carries over untouched, and walks through the path to start using groups and cascades once you upgrade. If you are coming from a different affiliate plugin rather than upgrading an existing Siren install, start with the [Migration Overview](/documentation/migration/overview) instead. To move up a tier, see the plans on the [Siren product page](/product/siren) and upgrade your license there. This guide covers what changes once you have done that. ## The four tiers Siren ships in four tiers that build on each other: Lite, Essentials, Plus, and Pro. Each tier is cumulative. Plus includes everything in Lite and Essentials, and Pro includes everything in Lite, Essentials, and Plus, then adds the next layer of capability on top. Because the tiers are additive, upgrading never removes a feature you already use. Everything that worked on your current tier continues to work after you upgrade. The new tier only adds capabilities, it does not change the ones you already have. ## What Plus adds: collaborator groups Plus introduces the collaborator group as a first-class concept. A [collaborator group](/documentation/general/what-are-collaborator-groups) is a named set of collaborators that you can bind to a program or a distributor. Binding a group to a program makes every member of that group eligible for the program, so you manage eligibility by editing group membership instead of enrolling collaborators one at a time. At the Plus tier, collaborator groups use the **flat** structure. A [flat group](/documentation/collaborator-group-structures/flat) is an unordered set of members with no hierarchy between them. Every member is eligible for whatever the group is bound to, and there is no notion of one member sitting above or below another. Plus owns the parts of the collaborator group system that do not depend on hierarchy: - The collaborator group itself and the storage behind it. - The flat structure. - The bindings that connect a group to a [program](/documentation/general/what-are-programs) or a [distributor](/documentation/general/what-are-distributors), which gate eligibility through group membership. This is enough to manage eligibility by group. It is not enough to run a cascade, because a cascade needs members arranged in a hierarchy. That arrives in Pro. ## What Pro adds: hierarchy and cascades Pro adds the hierarchical structures and the cascade calculations that operate over them. A flat group has no layers, so there is no direction to walk and nothing to credit beyond the member who triggered the event. Pro changes that by adding two ordered structures: - [Linear chain](/documentation/collaborator-group-structures/linear-chain), where members are arranged in a single ordered line by position. - [Parent-child](/documentation/collaborator-group-structures/parent-child), where members are arranged in a tree by depth. Both of these structures provide layers, which is what makes a [cascade](/documentation/general/what-is-a-cascade) possible. A cascade walks a hierarchical group and credits the triggering collaborator's peers layer by layer, in a chosen direction. Walking toward the top of the chain is an [upline cascade](/documentation/calculation-strategies/upline-cascade). Walking toward the leaves is a [downline cascade](/documentation/calculation-strategies/downline-cascade). The triggering collaborator is never credited by the cascade itself. Pro provides the cascade calculation strategies on both sides of the system, so a cascade can run on a program engagement or on a distributor metric. Because a flat group has no layers, cascade calculations only have something to walk when the bound group uses one of the Pro structures. This is why the calculation picker hides the cascade options when a flat group is bound. For the details of how the picker matches calculations to structures, see [Why some calculation methods disappear](/documentation/general/calc-capability-matching). ## Your existing setup keeps working Upgrading does not touch your current configuration. The tiers are cumulative, so every program, distributor, and rule you have already configured behaves exactly as it did before. In particular, [program groups](/documentation/general/what-are-program-groups) are unaffected. A program group bundles related programs and decides which one fires per conversion. It is a separate concept from a collaborator group, and it lives in the lower tiers. Adding collaborator groups does not replace program groups or change how they resolve. The two systems do different jobs and run side by side. Collaborator groups also bind eligibility in addition to your existing eligibility rules rather than overriding them. A collaborator who was already eligible for a program through direct enrollment stays eligible. Binding a group to that same program adds the group's members as an additional eligible set. Neither path masks the other. Being eligible through both paths does not pay a collaborator twice. Eligibility decides which programs a collaborator can earn from, and the program credits them once per conversion through its own calculation. The two paths only put the collaborator in the eligible set, they do not stack credits. So an upgrade is additive in both senses. No existing data is rewritten, and the new eligibility mechanism stacks on top of what you already have. ## The path to start using groups After upgrading to Plus, the path to managing eligibility by group is: 1. Create a collaborator group and add members to it. See [Create a collaborator group](/documentation/getting-started/create-a-collaborator-group) for the step-by-step walkthrough. 2. Bind the group to a [program](/documentation/general/what-are-programs) or a [distributor](/documentation/general/what-are-distributors). Every member of the group becomes eligible for the bound program or distributor. 3. Manage eligibility going forward by adding and removing members rather than enrolling collaborators individually. At the Plus tier the group's structure is flat, which is the right choice when membership is all you need. For more on when flat is the correct structure, see [Choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure). ## The path to start using cascades Cascades require the Pro tier, because they depend on the hierarchical structures Pro adds. Once you are on Pro: 1. Give the group a hierarchical structure. Switch the group from flat to [linear chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child), depending on whether your members form a single ordered line or a tree. 2. Set each member's structural position. A linear chain orders members by position, and a parent-child tree arranges them by depth. Switching a group's structure does not migrate per-member structural metadata, so after a structure change you need to set positions or parents on the members for the new structure to read them. 3. Configure a cascade calculation on the program engagement or distributor metric. Choose [upline cascade](/documentation/calculation-strategies/upline-cascade) to walk toward the top of the hierarchy or [downline cascade](/documentation/calculation-strategies/downline-cascade) to walk toward the leaves. With a hierarchical group bound, the picker shows these options. With a flat group bound, it hides them. 4. Set the per-layer points for the cascade. See [Configure cascade payouts](/documentation/getting-started/configure-cascade-payouts) for the full walkthrough. For help deciding which calculation strategy fits a given program, see [Choosing a calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy). ## Summary of what each tier adds A short reference for what becomes available as you move up. | Tier | What it adds for collaborator groups | |---|---| | Lite, Essentials | Programs, distributors, program groups, and the rest of the core incentive system. No collaborator groups. | | Plus | The collaborator group, the flat structure, and group-to-program and group-to-distributor bindings for eligibility. | | Pro | The linear-chain and parent-child structures, the layered structure behind them, and upline and downline cascade calculations on both programs and distributors. | ## Upline cascade calculation Source: https://www.sirenaffiliates.com/documentation/calculation-strategies/upline-cascade Walk the chain above the triggering collaborator and emit per-layer scores. Upline cascade credits the people above the triggering collaborator. When a trigger fires, Siren walks the chain upward from the person who caused it and pays each layer at the score you configured for that layer. ## How it works A trigger fires on one collaborator. They made a sale, they hit a metric, whatever the program watches. Upline cascade starts from that collaborator's position in the bound [CollaboratorGroup](/documentation/general/what-are-collaborator-groups) and walks toward the top. Layers are 1-indexed and counted from the trigger outward. Layer 1 is the direct upline, one step above the triggering collaborator. Layer 2 is the layer above that. The cap is layer 5. For each layer the cascade visits, it reads `pointsAtLayer{N}` from the configured args and credits the collaborator at that layer at that score. Going up, each collaborator has a single parent, so an upline layer always holds exactly one peer, in both a [linear chain](/documentation/collaborator-group-structures/linear-chain) and a [parent-child](/documentation/collaborator-group-structures/parent-child) tree. A layer holding several peers is a [downline](/documentation/calculation-strategies/downline-cascade) behavior, where one collaborator can have many children. The per-layer value is a score, not a dollar amount. It sets each collaborator's weight, and the program's incentive turns those weights into the actual payout. Pairing the cascade with a [performance-weighted pool](/documentation/distribution-structures/performance-weighted-pool) splits a reward pool in proportion to the scores, so the layer values decide each person's share of that pool. A 0 (or unset) value at any layer stops the cascade right there. If you set `pointsAtLayer1: 100`, `pointsAtLayer2: 50`, `pointsAtLayer3: 0`, the cascade walks two layers and stops. It does not jump over the 0 to look at layer 4. The triggering collaborator is never credited by an upline cascade. They caused the event, so the payout flows to the people above them. If an ancestor in the line is suspended or deleted, that layer earns nothing and the cascade keeps climbing to the next ancestor, who is still credited at their own layer's rate. For example, if the layer-2 grandparent is suspended, layer 2 pays no one, but layer 3 still credits the great-grandparent at the layer-3 score. The skipped layer's points are not reassigned to anyone else. If the bound CollaboratorGroup doesn't expose layers (a flat group, for example), upline cascade has nothing to walk and emits no credits. It fails closed. It emits nothing and never falls back to a single Fixed credit. The picker on the Programs Edit and Distributors Edit screens hides upline cascade when a flat group is bound, so you shouldn't run into this in normal use, but the picker is the only guard. If a cascade is already saved and the bound group is later changed to flat, deleted, or unbound, the calc keeps the cascade strategy and pays out zero rather than reverting to Fixed. See [Why some calculation methods disappear](/documentation/general/calc-capability-matching) for how the picker filters, and [Cascade troubleshooting](/documentation/calculation-strategies/cascade-troubleshooting) for the states that produce zero payouts after configuration. ## When to use it Picture a rep who closes a deal. Their manager earns a piece of that sale, and the manager's director earns a smaller piece above that. The credit flows upward from the producer to the people responsible for them, which is exactly what upline cascade does. That same shape covers a tiered sales hierarchy, where the people positioned above a producer earn a share and the people above them earn a smaller share above that. It also extends to the deeper override structures common in real estate, insurance, and brokerage hierarchies, where principals are paid a percentage of what the agents beneath them produce across several layers. In every one of these cases the rule is the same: the people positioned above a producer share in what that producer brings in. If you want the credit to flow the other direction, from the triggering collaborator down to the people they manage or lead, use [downline cascade](/documentation/calculation-strategies/downline-cascade) instead. If you want every member of a group to get the same flat amount regardless of position, use [fixed](/documentation/calculation-strategies/fixed). The [strategy overview](/documentation/calculation-strategies/choosing-a-calculation-strategy) walks through the trade-offs. ## Configuration Upline cascade takes five integer args: - `pointsAtLayer1`: points credited to the layer directly above the trigger. - `pointsAtLayer2`: points credited two layers above the trigger. - `pointsAtLayer3`: points credited three layers above the trigger. - `pointsAtLayer4`: points credited four layers above the trigger. - `pointsAtLayer5`: points credited five layers above the trigger. Set any layer to 0 to stop the cascade at that point. You don't need to fill in all five. Typical configurations use two or three layers and leave the rest at 0. The strategy only shows up in the picker when the bound CollaboratorGroup uses a `linearChain` or `parentChild` structure. Bind a flat group and the option disappears, because there are no layers to walk. See [Why some calculation methods disappear](/documentation/general/calc-capability-matching) for how that filtering works. On the program side, the bound group is read from the program config (config type `program`, key `collaboratorGroupId`) and the per-layer args are stored on the program engagement type (config type `programEngagementTypeArg`). On the distributor side, the same calc reads the bound group from the distributor config (config type `distributor`, key `collaboratorGroupId`) and reads the per-layer args from the distributor metric type (config type `distributorMetricTypeArg`). The arg keys are identical on both sides (`pointsAtLayer1` through `pointsAtLayer5`). The only difference is where the bound group and the args live, so a metric-side cascade needs a group bound to the distributor and is configured on the distributor's metric type. ## Worked example Picture a four-person linear chain: - `chain-one` at position 1 (top of the chain) - `chain-two` at position 2 - `chain-three` at position 3 - `chain-four` at position 4 (bottom of the chain) You configure upline cascade with `pointsAtLayer1: 100`, `pointsAtLayer2: 50`, `pointsAtLayer3: 25`, `pointsAtLayer4: 0`. `chain-four` makes a sale. The cascade starts at `chain-four` and walks upward. Layer 1 is `chain-three`, one step above the trigger, and earns 100 points. Layer 2 is `chain-two`, two steps above, and earns 50. Layer 3 is `chain-one`, three steps above, and earns 25. Layer 4 is 0, so the cascade stops. `chain-four` earns nothing from this calculation, because the trigger never credits the collaborator that caused it. A parent-child tree behaves the same way. Because upline follows a single line of ancestors, the trigger's parent is layer 1, its grandparent is layer 2, and so on, so this linear-chain example covers the parent-child case too. The shape is the same on the distributor side. If `chain-four` were a distributor instead of a program collaborator, the upline cascade would walk the bound distributor [CollaboratorGroup](/documentation/general/what-are-collaborator-groups) and credit `chain-three`, `chain-two`, and `chain-one` at 100, 50, and 25, but the credits would land in metric rows on the distributor side instead of engagement rows on the program side. The walk is the same. What receives the credit is what changes. For the conceptual background on how cascades work across program and distributor sides, see [What is a cascade](/documentation/general/what-is-a-cascade). ## Using Siren for Marketplaces Source: https://www.sirenaffiliates.com/documentation/getting-started/marketplaces How to use Siren as the commission engine for a multi-vendor marketplace, including product ownership, vendor tiers, payouts, and the WP_Query pattern for building a vendor storefront. Siren powers the commission and royalty side of multi-vendor marketplaces. It tracks which vendor owns which product, calculates per-sale royalties, and organizes payouts. What Siren doesn't do is marketplace UX. It isn't a storefront plugin, doesn't handle vendor onboarding flows, and doesn't render a "shop by vendor" page for you. That's usually where the first question shows up: "Does Siren replace WC Vendors, or do I run both?" The honest answer is that Siren is the commission engine and the product-ownership data layer, and the storefront UX is a separate concern. You can pair Siren with a marketplace plugin like WC Vendors or Dokan, or you can write a small amount of custom WordPress code and skip the marketplace plugin entirely. Both paths work, and which one fits depends on how much vendor self-service you need. This page walks through what Siren handles natively, where the gaps are, and the two realistic routes to a working marketplace. It's aimed at operators who already have (or plan to have) a WordPress site with WooCommerce, LifterLMS, EDD, or NorthCommerce powering the store. The assumption is that you need a commission engine that can handle multi-vendor revenue splits without forcing you to change your existing storefront. ## What Siren handles natively Product ownership is the foundation. Every WooCommerce product (or LifterLMS course, or EDD download) can have a collaborator owner, set via a field Siren adds to the product edit screen. When that product sells, Siren attributes the sale to the owning collaborator through the [Collaborator Product Sold](/documentation/general/collaborator-product-sold) trigger. This is the same mechanism the [product royalty program](/recipes/product-royalty-program) recipe uses, and it's the right fit for marketplaces because attribution happens automatically at checkout without anyone needing to click a referral link. That "no referral link" part is important. In a standard affiliate program, the customer has to arrive via a tracking URL or use a coupon code for the attribution to fire. In a marketplace, the customer is just shopping, and they probably don't know (or care) which vendor made the thing they're buying. Product ownership sidesteps the whole referral problem by attributing based on what was sold rather than how the customer got there. Per-sale royalty calculation uses the [Percentage of Transaction](/documentation/incentive-structures/percentage-of-transaction) incentive structure. Set the percentage that matches your vendor/platform split (70% to the vendor and 30% to the platform is common for handmade marketplaces, 50/50 for curated design marketplaces, and so on), and Siren runs the math on every sale. Commissions are calculated on line item totals, so shipping and taxes stay out of the calculation. The multi-vendor order case is where marketplace commission engines get interesting. If a customer buys three items from three different vendors in one order, each vendor earns their percentage on their specific line item. Nobody competes with anyone else for the same sale. This is what the "every binding wins" resolver does in the [marketplace vendor commission recipe](/recipes/marketplace-vendor-commission), and it's the critical difference between a marketplace program and a standard affiliate program where only one collaborator can earn per transaction. Vendor tiers are handled with [program groups](/documentation/general/what-are-program-groups). If your marketplace has "premium vendors earn 80%, standard vendors earn 70%" you can run two programs and enroll each vendor in the one that matches their tier. You can also use a single program and override the rate on individual collaborator profiles, but separate programs are cleaner when you want to see premium versus standard payouts at a glance. Program groups also let you switch vendors between tiers without having to rewrite their individual settings: move a vendor from the standard program to the premium program and the new rate kicks in immediately for future sales. Payouts follow Siren's standard fulfillment workflow. Group obligations by collaborator, export a CSV, mark paid in bulk. If you're paying 80 vendors on the first of every month, this is the same flow Siren uses for any other program. The only thing that's marketplace-specific is the sheer volume of obligations (one per vendor per period), and Siren's fulfillment view handles that fine. Sort by collaborator, review the totals, export. See [How to Pay Collaborators](/documentation/getting-started/how-to-pay-collaborators) for the step-by-step. Refunds in marketplaces work the same way as refunds in any Siren program. When a customer refunds an order, the conversion attached to that order is rejected, and if the obligation hasn't been fulfilled yet, it's rejected too. The [refund pipeline](/documentation/general/how-refunds-work) has more detail. The practical implication for marketplaces is that you should hold off on paying vendors until your refund window has passed. If your marketplace offers a 30-day return policy, wait at least 30 days between order and payout so refunds have time to roll in before money leaves your account. ## What Siren doesn't handle Here's the list of things you'll need to cover some other way: - Vendor storefronts (the "shop by vendor" page that lists all products from one vendor) - Vendor self-service product submission - Vendor profile pages and identity management - Vendor onboarding application flows - Tax form collection and 1099 generation - Vendor-specific email automation None of these are close to shipping. They're deliberately out of scope because they're things WordPress (or a dedicated marketplace plugin) already handles well, and Siren's job is the commission engine. If any of these are must-haves for your marketplace, you'll pair Siren with another tool to cover them. The rest of this page walks through what that pairing looks like. ## The two paths to a vendor storefront ### Path A: Use a marketplace plugin alongside Siren WC Vendors, Dokan, and similar plugins handle the storefront, vendor identity, and product management. Siren handles commission tracking. They coexist: the marketplace plugin manages the vendor-facing UX (dashboards, product submission, storefront pages), and Siren manages commission calculation and payouts. The mental model is "marketplace plugin for the front-end, Siren for the money." There's some overlap. Both WC Vendors and Siren want to know which products belong to which vendor. That's fine, because Siren's product ownership field can be set independently of whatever the marketplace plugin does. You can even script the assignment so the two systems stay in sync automatically, though for most sites a one-time import is enough. You might wonder why not just use WC Vendors for both the storefront and the commissions. The answer is that WC Vendors' commission engine is less flexible than Siren's. It handles flat percentages and not much else. If you want tiered commissions, bonus structures, program groups with resolver logic, or anything more complex than "vendor gets X percent of sales," Siren is the stronger tool. Running both gives you WC Vendors' storefront and Siren's commission engine, which is usually the right call for marketplaces that need more than a flat commission rate. This path is the right call when you need vendor self-service. If vendors should log in, add their own products, edit their own descriptions, and manage their own inventory without you touching anything, a marketplace plugin gets you there faster than writing custom code. ### Path B: Build a vendor storefront with a small WordPress customization Siren's product ownership is stored as standard WordPress data. That means you can query it with `WP_Query` and build a vendor storefront page yourself. A custom template page can show "all products owned by this collaborator" by passing the collaborator's ID to a meta query, looping through the results, and rendering whatever layout your theme wants. This is maybe 50 lines of PHP for a basic storefront, and it gives you complete control over the UX without depending on a marketplace plugin. The query looks roughly like this in concept: load the collaborator record by ID or slug, pull their linked product IDs from Siren's ownership metadata, then feed those IDs into a standard `WP_Query` to render them as a product grid. Anyone comfortable with WordPress theme development can put this together in an afternoon. The rest (pagination, filtering by category, sorting by price) is all standard WordPress patterns you probably already use elsewhere on the site. This path fits sites that already have a custom theme and want a tightly-integrated vendor experience. If your marketplace has a strong visual identity and you'd rather control every pixel of the vendor page layout, a custom template is the cleanest route. The trade-off is that vendors can't log in and edit their own products. You (or your team) add products on their behalf. For curated marketplaces where every listing is reviewed before going live, this is usually how the workflow runs anyway, so the lack of vendor self-service isn't actually a constraint. ## Bulk product ownership assignment For a marketplace with hundreds or thousands of products, assigning ownership one product at a time through the WordPress admin is slow. There's no bulk-assign UI in Siren today, which means you'll need a small workaround for the initial import. The fastest route is to script the assignment via Siren's REST API. A few lines of PHP (or a WP-CLI script) can read your products list and post ownership records. If you have a CSV of "product SKU, vendor collaborator ID" this is a one-afternoon job for anyone comfortable with WordPress development. The ownership records persist just like manual assignments do, so once the script runs, everything behaves the same from that point forward. If you'd rather not write the script yourself, describe your situation to [Beacon](/documentation/getting-started/what-is-beacon) and ask for a one-off import script. Beacon can generate the PHP and walk you through running it. For most marketplaces, the initial import is a one-time job, so the script is throwaway code that doesn't need to become part of your permanent codebase. Bulk product ownership assignment is on the roadmap as a future feature, so this workaround may not be necessary forever. For now, the REST API route is the fastest path from zero to a fully-assigned catalog. ## Splitting commission with an introducer Some marketplaces want a third party to earn commission alongside the vendor. An agent who recruited the vendor, a marketing partner who drove traffic to that vendor's page, a curator who vetted the vendor for inclusion. In this model, the vendor gets their usual 70%, the marketplace keeps a smaller cut, and the introducer takes the remainder. Siren can handle this with program groups or with parallel programs, depending on how the attribution should work. If the introducer should always earn a flat percentage on every sale from the vendors they introduced, run a second program that fires on the same "collaborator product sold" trigger but targets the introducer's collaborator profile. You'd need to set up a custom attribution rule so the introducer is credited based on the vendor ownership relationship, which usually requires a small custom integration. For most marketplaces, this level of complexity isn't worth it. Pay the introducer a flat fee per vendor signup instead, tracked manually as a one-time obligation. It's simpler, easier to explain to everyone involved, and doesn't require any custom code. ## A worked example Picture a handmade goods marketplace with 50 vendors, 1,500 products, and $30,000/month in GMV. The business split is 70/30 (vendor/platform). The founder wants automated commission tracking and a once-a-month payout workflow. The Siren setup: 1. Install the [marketplace vendor commission recipe](/recipes/marketplace-vendor-commission). This creates the program with a 70% Percentage of Transaction incentive and the "every binding wins" resolver (so every vendor with a product in an order gets paid, not just one). 2. Add each vendor as a collaborator. 3. Assign products to vendors via a one-time REST API import script. Roughly 1,500 products, a few hundred lines of CSV, one afternoon of work. 4. On the first of each month, generate a fulfillment, review the obligations by vendor, export the CSV, and upload it to whatever ACH tool the marketplace uses. The front-end remains whatever the site already had. No marketplace plugin swap, no vendor dashboard rewrite. The commission engine slots in behind the existing storefront and runs quietly. A few numbers for context. At $30,000/month in GMV with a 70/30 split, the platform is keeping $9,000/month and paying out $21,000 across 50 vendors. Average per-vendor payout is about $420, though in practice it'll be heavily skewed (a few top vendors earning most of the pool, a long tail earning smaller amounts). Siren's fulfillment export handles that distribution fine: obligations of any size roll up into the same CSV, and you can filter out vendors below a minimum payout threshold if you want to roll small amounts into the next cycle. ## When to pick this over a dedicated marketplace plugin If you're deciding between Siren + WordPress versus a dedicated marketplace SaaS (something like Arcadier or Sharetribe), the trade-off is control versus speed. Marketplace SaaS platforms ship with vendor dashboards, application flows, and tax form collection out of the box. Siren on WordPress gets you the commission engine and leaves everything else to you. For a marketplace that needs to ship in two weeks with 200 vendors, the SaaS platform is faster. For a marketplace that already has a WordPress site and wants full control over the experience, Siren is the right engine. The other trade-off is cost. Marketplace SaaS typically charges per-vendor or takes a percentage of GMV. Siren's pricing is flat, which means as your marketplace grows, your Siren cost stays the same while the SaaS cost scales with you. For marketplaces above a certain GMV, this difference alone pays for the additional setup work. ## Closing pointers For the deeper concept, read [What is an Engagement](/documentation/general/what-is-an-engagement) and [Collaborator Product Sold](/documentation/general/collaborator-product-sold). For the ready-to-apply configuration, see the [marketplace vendor commission recipe](/recipes/marketplace-vendor-commission) or the [product royalty program recipe](/recipes/product-royalty-program). For payout workflow details, see [How to Pay Collaborators](/documentation/getting-started/how-to-pay-collaborators). If your marketplace has a non-standard configuration (multiple tiers, line-item filters by SKU, conditional rates based on product category), describe it to [Beacon](/documentation/getting-started/what-is-beacon) and ask for a custom recipe. ## Vocabulary Translation Source: https://www.sirenaffiliates.com/documentation/general/vocabulary-translation How Siren's terms map to the vocabulary you already know from other affiliate platforms and from non-affiliate use cases like LMS, marketplace, and SaaS. Siren uses different vocabulary than most affiliate platforms because it does more than traditional affiliate tracking. The same install can run an affiliate program, an instructor royalty program, a marketplace vendor payout, a sales team commission, and a referral bonus, all at once. That broader scope needs terms that don't assume everyone is an "affiliate" and every action is a "referral." This page is a translation layer. If you're coming from another affiliate plugin, the first table maps Siren's terms to the ones you already know. If you're using Siren for something that isn't an affiliate program at all, the second table maps the same terms to whatever your team probably calls these concepts. Use whichever row makes the rest of the documentation click faster. Once a term starts to feel natural, you can drop the translation and use Siren's vocabulary directly. ## Coming from another affiliate platform Each row is a Siren concept. The remaining columns show the closest equivalent term in other affiliate platforms. A dash means there's no direct equivalent in that platform. | Siren term | AffiliateWP | Tapfiliate | Refersion | Post Affiliate Pro | Commission Junction | Impact | |---|---|---|---|---|---|---| | [Collaborator](/documentation/general/what-is-a-collaborator) | Affiliate | Affiliate | Affiliate / Ambassador | Affiliate | Publisher | Partner | | [Program](/documentation/general/what-are-programs) | Global rate + overrides | Program | Offer | Campaign | Advertiser program | Program / Contract | | [Engagement](/documentation/general/what-is-an-engagement) | -- | -- | -- | -- | -- | -- | | [Opportunity](/documentation/general/what-is-an-opportunity) | Visit | Click | Click / Visit | Click | Click | Click | | [Conversion](/documentation/general/what-is-a-conversion) | Referral | Conversion | Conversion / Sale | Commission | Transaction | Action | | [Obligation](/documentation/general/what-are-obligations) | Unpaid referral | Pending commission | Pending commission | Pending commission | Pending transaction | Pending action payout | | [Fulfillment](/documentation/general/what-is-a-fulfillment) | Payout batch | Payout | Payout | Payout | Advertiser invoice | Payout | | [Distributor](/documentation/general/what-are-distributors) | Tiered rates add-on / Leaderboard | Performance bonus | -- | Performance rewards | -- | Dynamic payouts | | [Program Group](/documentation/general/what-are-program-groups) | -- | -- | -- | Category (loosely) | -- | -- | | Tracking ID / [Alias](/documentation/resource-reference/aliases) | Affiliate slug | Affiliate ID | Affiliate code | Reference ID | SID | Partner ID | | Coupon code ([Coupon tracking](/documentation/general/coupon-tracking)) | Affiliate coupon | Coupon code | Coupon code | Coupon code | Promo code | Promo code | | [Manual attribution](/documentation/general/manual-attribution) | Add referral manually | Manual conversion | Manual commission | Manual tracking | Event upload | Manual action | | Rejected conversion | Rejected referral | Voided conversion | Cancelled | Declined | Corrected | Reversed action | | Renewal conversion | Recurring Referrals add-on | Recurring | Subscription conversion | Recurring commission | -- | Subscription action | | [Collaborator-owned product](/documentation/general/line-item-filters) | -- | -- | -- | -- | -- | -- | A few things to note about the gaps: Engagements don't have an equivalent anywhere else because most platforms model the world as "one click leads to one sale." Siren tracks engagements as per-program credit claims, which is what lets a single visit earn credit in multiple programs at once. The closest concept in other tools is a click, but clicks in those tools aren't scoped to programs. Program groups are a Siren-specific construct. Other platforms handle mutually exclusive rates by editing a single global rule or by layering per-affiliate overrides on top of a base rate. Program groups make the mutual exclusion explicit and visible in the program list. Distributors don't have a clean equivalent in most platforms. They're scheduled, aggregate bonus payouts based on tracked metrics, and most affiliate tools bolt this on through a "tiered rates" or "leaderboard" add-on rather than treating it as a first-class concept. ## Coming from a non-affiliate background Siren is often used for things that aren't affiliate programs at all. If you run a course platform, a marketplace, a SaaS business, or an internal sales team, the vocabulary below maps Siren's terms to whatever you probably call them already. | Siren term | LMS operator | Marketplace operator | SaaS founder | Sales manager | |---|---|---|---|---| | [Collaborator](/documentation/general/what-is-a-collaborator) | Instructor | Vendor / Seller | Partner / Reseller | Sales rep | | [Program](/documentation/general/what-are-programs) | Revenue share plan | Vendor payout plan | Partner program | Commission plan | | [Engagement](/documentation/general/what-is-an-engagement) | Course completion / lesson view | Product listing view / click | Lead / signup | Lead / touch | | [Opportunity](/documentation/general/what-is-an-opportunity) | Enrolled student (pre-payment) | Visitor on product page | Trial user | Qualified lead | | [Conversion](/documentation/general/what-is-a-conversion) | Paid student | Sale | Paid customer | Closed deal | | [Obligation](/documentation/general/what-are-obligations) | Payout owed to instructor | Commission earned | Partner commission | Sales commission | | [Fulfillment](/documentation/general/what-is-a-fulfillment) | Monthly instructor payout | Vendor payout cycle | Partner payment run | Commission payroll | | [Distributor](/documentation/general/what-are-distributors) | Revenue share pool | Marketplace rewards pool | Quarterly partner bonus | Sales bonus / SPIF | | [Program Group](/documentation/general/what-are-program-groups) | Instructor tier set | Vendor tier set | Partner tier set | Rep tier set | | Coupon code | Course promo code | Vendor discount code | Signup code | Deal code | Siren doesn't require you to memorize these mappings. They're here so that when the docs talk about "an obligation being rejected because of a refund," you can mentally translate it to "the commission we owed our sales rep gets voided because the deal fell through" without pausing. As you use Siren, the native terms will start feeling natural and the translation layer can drop away. ## Where to go from here Once a term clicks, read the canonical definition in the User Guide. These are the concept docs for the terms in the tables above: - [What are Programs?](/documentation/general/what-are-programs) - [What is a Collaborator?](/documentation/general/what-is-a-collaborator) - [What is an Engagement?](/documentation/general/what-is-an-engagement) - [What is an Opportunity?](/documentation/general/what-is-an-opportunity) - [What is a Conversion?](/documentation/general/what-is-a-conversion) - [What are Obligations?](/documentation/general/what-are-obligations) - [What is a Fulfillment?](/documentation/general/what-is-a-fulfillment) - [What are Distributors?](/documentation/general/what-are-distributors) - [What are Program Groups?](/documentation/general/what-are-program-groups) If you're actively migrating from AffiliateWP, the [AffiliateWP migration guide](/documentation/migration/affiliatewp) has a deeper mapping with status translations and database table references. ## Walker capabilities Source: https://www.sirenaffiliates.com/documentation/extensions/walker-capabilities How calculation strategies and structure resolvers negotiate compatibility through capability strings, and how to add new ones. Programs and distributors bind to a [collaborator group](/documentation/general/what-are-collaborator-groups) with a particular structure. Some calc strategies, [upline cascade](/documentation/calculation-strategies/upline-cascade) and [downline cascade](/documentation/calculation-strategies/downline-cascade), only make sense against a group whose walk yields layered steps. A flat-bound program has nothing to walk through, so showing Upline Cascade in its picker would be a trap. Capabilities are how Siren keeps that honest without hardcoding a compatibility table between every calc and every structure. The producer side advertises what its walker yields, the consumer side declares what it needs, and the picker filters by set-subset. ## The two marker interfaces The contract lives in two sibling marker interfaces in `lib/Core/Core/Interfaces/`. Both stay in Core so the registry plumbing doesn't have to know about Plus or Pro to surface the fields. `RequiresWalkerCapabilities` is the consumer-side marker. Calculation strategies, both the engagement-side `EngagementCalculationStrategy` and the metric-side `MetricCalculationStrategy`, implement it when they need particular data on the walker steps they receive. ```php namespace Siren\Core\Core\Interfaces; interface RequiresWalkerCapabilities { /** * @return string[] */ public function getRequiredWalkerCapabilities(): array; } ``` `HasProvidedWalkerCapabilities` is the producer-side marker. Structure resolvers (the `flat` resolver in Plus, the `linearChain` and `parentChild` resolvers in Pro, plus any third-party resolver, see [custom structure resolvers](/documentation/extensions/collaborator-group-structures)) implement it to advertise what their directional walkers carry. ```php namespace Siren\Core\Core\Interfaces; interface HasProvidedWalkerCapabilities { /** * @return string[] */ public function getProvidedWalkerCapabilities(): array; } ``` Both surface through `RegistryResponseInterceptor`, which checks `instanceof` against each marker and emits `requiredWalkerCapabilities` and `providedWalkerCapabilities` arrays on the registry responses the frontend consumes. The picker on the program-edit and distributor-edit screens reads both lists, then hides any calc whose required capabilities aren't a subset of the bound group's provided capabilities. In plain terms, a structure may provide more capabilities than a calc needs, and a calc is hidden only when it needs a capability the bound group does not provide. A calc requiring `[hasLayer]` is offered against a structure providing `[hasLayer, hasWeight]`, but hidden against one providing `[]`. With no group bound, no filter applies, so every calc shows. See [calc capability matching](/documentation/general/calc-capability-matching) for the operator-facing view of this behavior. ## The WalkerStep contract Structure resolvers yield `WalkerStep` value objects, not raw `CollaboratorGroupMember` instances. The base contract is minimal: a member, period. ```php namespace Siren\Pro\Core\Groups\Structure\Walkers\Interfaces; interface WalkerStep { public function getMember(): CollaboratorGroupMember; } ``` Everything else a step might carry (a layer, a weight, a distance) lives on optional marker interfaces the step implements. This is the same composable shape Siren uses elsewhere (`HasInterceptors`, `HasMiddleware`, and so on). Capability is declared by interface, not by inheritance. Today the only marker is `HasLayer`: ```php namespace Siren\Pro\Core\Groups\Structure\Walkers\Interfaces; interface HasLayer { public function getLayer(): int; } ``` The hierarchical walker step Pro ships implements both: ```php final readonly class HierarchicalWalkerStep implements WalkerStep, HasLayer { public function __construct( public CollaboratorGroupMember $member, public int $layer ) {} public function getMember(): CollaboratorGroupMember { return $this->member; } public function getLayer(): int { return $this->layer; } } ``` The pair to keep straight: `hasLayer` is the *capability id* (a string the picker filters by), and `HasLayer` is the *marker interface* (a type the cascade service checks with `instanceof` to read `getLayer()` safely). The two are kept in sync through the `WalkerCapability` enum in Pro: ```php namespace Siren\Pro\Core\Groups\Structure\Enums; class WalkerCapability { use Enum; public const HAS_LAYER = 'hasLayer'; } ``` The picker never sees the interface. The runtime never sees the string. The enum is the seam where one becomes the other. ## Writing a calc that requires a capability A Pro-tier metric calc that needs layered steps looks like this: ```php namespace Siren\Pro\Core\Metrics\Calculations; use Siren\Core\Core\Interfaces\RequiresWalkerCapabilities; use Siren\Metrics\Core\Interfaces\MetricCalculationStrategy; use Siren\Metrics\Core\Models\MetricCalculationContext; use Siren\Pro\Core\Groups\Structure\Enums\WalkerCapability; class UplineCascadeMetricCalculation implements MetricCalculationStrategy, RequiresWalkerCapabilities { public function calculate(MetricCalculationContext $context): array { return $this->cascade->cascade( $context, fn($structure, int $triggerId) => $structure->getUplineWalker($triggerId) ); } public function getRequiredWalkerCapabilities(): array { return [WalkerCapability::HAS_LAYER]; } // getId(), getName(), getDescription(), getRequiredArgs() omitted } ``` Implementing the marker is the entire opt-in. The interceptor finds it, the picker filters on it, and the cascade service downstream can trust that any step it gets implements `HasLayer` because the operator was never offered an incompatible combination in the first place. The engagement-side equivalent (`UplineCascadeEngagementCalculation`) is structurally identical: same marker, same enum, different strategy interface and different service. That trust is a convenience, not a guarantee. The picker only governs new picks. A calc saved against a compatible group still runs if the group is later switched to a structure that no longer provides the capability, and then its walker yields steps without it. So a calc that reads a capability still guards each step with `instanceof` (as the example below does) and treats a step that lacks the capability as fail-closed, skipping it rather than calling a method that is not there. ## Built-in capabilities One capability ships today. ### hasLayer ``` ID: 'hasLayer' (WalkerCapability::HAS_LAYER) Tier: Pro ``` Declares that a walker step knows its layer, the 1-indexed distance from the triggering collaborator. The linear chain and parent-child structures provide it, and the upline and downline cascade calc strategies require it. The `HasLayer` marker interface and the `WalkerCapability` enum both live in Pro. ## Adding a new capability Say you want a weighted walker, a structure where each step carries a numeric weight, and a calc that pays out proportionally to that weight. The full path from zero to a filtered picker: 1. Add a constant to a capability vocabulary enum. If you're extending the first-party Pro work, add to `WalkerCapability`: `public const HAS_WEIGHT = 'hasWeight';`. If you're a third-party extension, define your own enum (same shape). The picker doesn't care where the string came from, only what it is. 2. Define a marker interface, `HasWeight` with a `getWeight(): float` method. Keep it tier-appropriate. The marker doesn't need to live in Core. 3. Have your walker step value object implement both `WalkerStep` and `HasWeight`. Carry the weight as a constructor argument. 4. Have your structure resolver implement `HasProvidedWalkerCapabilities` and return `['hasWeight']` from `getProvidedWalkerCapabilities()`. If your resolver also walks layered, return `['hasLayer', 'hasWeight']`. 5. Have your calc strategy implement `RequiresWalkerCapabilities` and return `['hasWeight']` from `getRequiredWalkerCapabilities()`. At runtime, type-check each step with `$step instanceof HasWeight` before reading the weight. The picker auto-filters from here. No frontend changes. No registry rebind. Your calc shows up exactly when a compatible structure is bound, and only then. These steps cover the capability handshake only. Your structure resolver and your calc strategy still have to be registered through their own events before any of this surfaces. See [custom structure resolvers](/documentation/extensions/collaborator-group-structures) and [custom calculation strategies](/documentation/extensions/calculation-strategies). And the walker step in step 3 does not invent its weight: your resolver computes it as it walks and passes it to the step's constructor, the same way the hierarchical step receives its layer. The capability id is a bare string, and nothing checks that your producer and consumer spell it the same way. A typo on either side makes the subset check fail, and your calc silently disappears from the picker with no error. So publish the id as a single shared constant that both your structure resolver and your calc depend on, rather than minting it on each side. When a calc you expect is missing, compare the `requiredWalkerCapabilities` and `providedWalkerCapabilities` arrays on the registry responses (the `RegistryResponseInterceptor` emits both) to see which string did not match. ## Layering note The marker interfaces, `RequiresWalkerCapabilities` and `HasProvidedWalkerCapabilities`, sit in Core because they're tier-neutral plumbing. The interceptor that surfaces them sits in Core for the same reason. But the concrete vocabulary (`WalkerCapability::HAS_LAYER`) lives in Pro, because the cascade work that needed it is a Pro feature. The `HasLayer` marker also lives in Pro, alongside the hierarchical walker step that implements it and the cascade services that consume it. The takeaway for extension authors: you can declare your own capability ids at any tier without touching Pro. The Core interfaces accept opaque strings on purpose. As long as your producer and your consumer agree on the string, the picker handles the rest. > **For operators:** the user-facing view of this mechanism is documented under [calc capability matching](/documentation/general/calc-capability-matching) and [what is a cascade](/documentation/general/what-is-a-cascade). ## What are Collaborator Groups? Source: https://www.sirenaffiliates.com/documentation/general/what-are-collaborator-groups A reusable, dynamic membership of collaborators that programs and distributors can bind to instead of listing collaborators directly. A collaborator group is a named, dynamic membership of [collaborators](/documentation/general/what-is-a-collaborator) that programs and distributors can bind to as a single unit. Instead of attaching ten individual affiliates to a program one at a time, you create a collaborator group, drop those ten affiliates into it, and tell the program to use the group. Add an eleventh affiliate to the group later and the program picks them up automatically. Remove someone and they stop participating without anyone touching the program itself. Collaborator groups exist because the alternative, pasting the same list of collaborators onto every program and distributor that should include them, falls apart the moment that list changes. A collaborator group keeps the membership in one place and lets the things that care about it stay pointed at the group, not the people. ## How it's different from a program group The names sound similar, so it's worth pinning down the difference up front. A [program group](/documentation/general/what-are-program-groups) bundles programs so that only one of them runs per conversion. It's a mutual-exclusion mechanism for overlapping reward rules. A collaborator group bundles collaborators so that programs and distributors can bind to the bundle instead of the individuals. It's a membership mechanism for reusable rosters. Program groups answer "which program should this conversion pay?" Collaborator groups answer "who participates in this program in the first place?" The two concepts solve different problems and you can use both in the same setup without conflict. ## How it works A collaborator group has a name, a list of members, and a structure. The members are just collaborators, anyone with an active collaborator record can be added. The structure is what shape the group has internally, and Siren ships three of them. A [flat group](/documentation/collaborator-group-structures/flat) is an unordered bag of members where everyone is a peer, which makes it the default for Plus and the right fit for most simple cases. A [linear chain](/documentation/collaborator-group-structures/linear-chain) orders members top-to-bottom by a `position` field so the group carries an explicit sequence, while a [parent-child group](/documentation/collaborator-group-structures/parent-child) arranges members into a tree, with each member pointing at a parent collaborator above them. For most operators the structure is set once when you create the group and never touched again. The reason it matters is that the structure controls how a [cascade](/documentation/general/what-is-a-cascade) reads the group when one is running. Flat groups don't expose layers, so cascade calculations can't run against them, while linear chain and parent-child groups do. If you're not using cascades, flat is fine and the structure choice mostly goes away. The page on [choosing a collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) walks through the tradeoffs in detail. Binding works the same way regardless of structure. A [program](/documentation/general/what-are-programs) can bind to a collaborator group, and so can a [distributor](/documentation/general/what-are-distributors). Once bound, the program or distributor treats the group's current membership as its collaborator list. There's no copy step. The binding is live, and membership changes propagate immediately. Because it is live, a membership change takes effect on every program and distributor bound to the group at once, so removing a collaborator who is shared across several bound programs drops them from all of those at the same moment. Double-check before removing someone who is shared. ## When to use one A collaborator group earns its keep in a few related situations. The clearest is reuse: any time you would otherwise paste the same collaborators onto two or more programs or distributors, the duplication starts to bite, and even three programs that share the same five affiliates feel that pull quickly. The same group also absorbs churn, because as new affiliates join, old ones leave, and top performers move up a tier, you change the roster in one place instead of editing every program. Beyond reuse and churn, a group becomes the foundation for hierarchy, since anything resembling a tiered or override-based payout needs a structured collaborator group for a [cascade](/documentation/general/what-is-a-cascade) to walk. And once any of those reasons applies, the group keeps the operator UI simple as a side effect, because a program bound to a 50-member group reads as one binding on the program edit screen rather than 50 individual attachments. If you have a single program with three hand-picked collaborators and the list never changes, you don't need a group. Attach the collaborators directly and move on. Collaborator groups earn their keep when there's reuse, churn, or hierarchy in play. ## Lifecycle Working with a collaborator group looks like this in practice: 1. **Create the group.** Give it a name, pick a structure, save it. The [create a collaborator group tutorial](/documentation/getting-started/create-a-collaborator-group) walks through the screens. 2. **Add members.** Pick collaborators from the list and add them to the group. For linear chains, set each member's position. For parent-child, set each member's parent. 3. **Bind it.** On a program's edit screen, or on a distributor's edit screen, point the collaborator-group binding at your group. From that point on, the program or distributor treats the group's roster as its participant list. 4. **Maintain membership.** As people join or leave, add and remove them from the group. Bound programs and distributors pick up the change without any further edits. Before you delete a group, clear any program or distributor binding first. Deleting a bound group does not detach it and does not warn you. The delete goes through, the group and its members are removed, but the binding stays on record pointing at a group that no longer exists, and the next payout cascade on that program or distributor fails closed and credits no one, with no error. So find every program and distributor pointed at the group, re-point or unbind each one, and only then delete. > **For developers:** A collaborator group resolves through a structure resolver registered against the `CollaboratorGroupStructure` capability. See the [collaborator groups resource reference](/documentation/resource-reference/collaborator-groups) and the [custom structure resolvers](/documentation/extensions/collaborator-group-structures) extension page for the full API. ## What are Distributors? Source: https://www.sirenaffiliates.com/documentation/general/what-are-distributors Distributors track collaborator performance over time and pay out on a schedule. This page explains how they work, what the lifecycle looks like, and how they differ from programs. import EventFlow from "@/components/content/EventFlow.astro"; A distributor tracks [collaborator](/documentation/general/what-is-a-collaborator) performance over time and creates [obligations](/documentation/general/what-are-obligations) on a schedule rather than on every sale. Unlike [programs](/documentation/general/what-are-programs), which fire the moment a customer converts, a distributor accumulates data across a tracking period and then pays out in a batch when the period ends. This opens up reward structures that programs can't express. Anything that depends on aggregate performance (leaderboards, bonuses, profit shares, revenue splits) needs a distributor because the reward can't be calculated until the period is over. ## When to use a distributor A distributor is the right tool whenever you can answer three questions: what measurable thing are you tracking, how much are you paying for it, and on what schedule should the payout happen? If you can define all three, you can build a distributor around it. The two most common shapes are profit shares and bonuses. A profit share pays collaborators a cut of revenue based on their contribution to a period, like a course platform paying creators a share of subscription revenue based on how many students completed their courses. The [content creator profit share recipe](/recipes/content-creator-profit-share) is a working example. A bonus pays out to top performers, like rewarding the affiliate who drove the most sales last month. The [monthly sales bonus recipe](/recipes/monthly-sales-bonus) shows how that's built. ## The lifecycle of a distribution ## Distribution structures A distribution structure defines who gets paid when a distribution triggers, and how the reward pool is divided among the winners. See [Distribution Structures](/documentation/distribution-structures/choosing-a-distribution-structure) for the available structures and how to pick one. ## Tracking events and metric scores Distributors measure collaborator activity through engagement triggers. Each distributor is configured with the event types it cares about, and each event type has a configurable point value. As events fire during the tracking period, Siren accumulates a metric score for each collaborator by adding up the points from every qualifying event. If a distributor tracks both course completions and lesson completions, you might weight course completions at 10 points and lesson completions at 1 point. A collaborator whose students complete 5 courses and 20 lessons during the period ends up with a score of 70. When the distribution triggers, that score is what the distribution structure uses to decide what the collaborator is owed. The full list of engagement triggers distributors can track (course completions, product sales, blog post visits, coupon uses, site visits, and more) lives in the engagement trigger reference. [Site Visited](/documentation/general/site-visited) is a good starting point if you want to see how a single trigger is documented. ## Cascading metric scores across a hierarchy A distributor isn't limited to scoring the collaborator who triggered the event. Bind a distributor to a [collaborator group](/documentation/general/what-are-collaborator-groups) with a chain or tree structure, switch the metric type's calculation method from Fixed to [Upline Cascade](/documentation/calculation-strategies/upline-cascade) or [Downline Cascade](/documentation/calculation-strategies/downline-cascade), and a single trigger emits scores up (or down) the hierarchy at your configured per-layer values. When the distribution settles, those cascade-emitted scores feed into the pool the same way direct scores do. A tiered team's manager can collect a slice of the pool from every sale their reports made during the period without you tracking each contribution by hand. See [What is a cascade](/documentation/general/what-is-a-cascade) for the full mental model. > **For developers:** Distributors fire a set of events across their lifecycle. See the [distribution events reference](/documentation/developer-reference/events-distributions) for the full pipeline. ## What Are Obligations? Source: https://www.sirenaffiliates.com/documentation/general/what-are-obligations An obligation is Siren's record that a collaborator is owed money. This page explains how obligations are created, their statuses, and how they become payouts. An obligation is Siren's record that a [collaborator](/documentation/general/what-is-a-collaborator) is owed a specific amount of money for a specific reason. Each obligation ties one collaborator to one [conversion](/documentation/general/what-is-a-conversion) (or to one distribution period, in the case of [distributors](/documentation/general/what-are-distributors)) and carries the calculated reward amount. Think of obligations as Siren's accounts-payable backlog for your affiliate program. ## How obligations are created Most obligations come from approved conversions. When a customer buys something and the conversion that results is approved, Siren runs the program's incentive structure against the underlying transaction, works out the reward amount, and creates an obligation for the collaborator. If the program is configured to auto-approve conversions, this happens the moment the sale lands. If you hold conversions in pending for review, the obligation is created when you approve them. Distributors create obligations on a different cadence. Instead of creating one obligation per sale, a distributor accumulates metric data over a tracking period and then creates obligations in a batch when the distribution triggers on schedule. ## Obligation statuses Every obligation carries one of three statuses. A *pending* obligation is waiting to be included in a [fulfillment](/documentation/general/what-is-a-fulfillment). A *complete* obligation has been tallied into a fulfillment and turned into a payout record. A *rejected* obligation has been invalidated and will not be paid, which happens when you reject it manually or when a [refund](/documentation/general/how-refunds-work) automatically reverses the conversion that created it. Rejecting an obligation is the last checkpoint before money leaves your business. It's how you catch non-compliant conversions, fraudulent activity, or discrepancies that didn't surface during conversion review. ## Paying obligations Obligations don't become real payments on their own. You pay them by creating a fulfillment, which gathers all the pending obligations you want to settle, totals them by collaborator, and turns each total into a payout record. Once an obligation is rolled into a fulfillment, its status moves to complete and it won't be picked up by future fulfillments. The [What is a Fulfillment?](/documentation/general/what-is-a-fulfillment) page walks through the fulfillment side of this process in more detail. > **For developers:** This concept maps to the [`ObligationIssued`](/documentation/developer-reference/events-payments/obligation-issued) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What are Program Groups? Source: https://www.sirenaffiliates.com/documentation/general/what-are-program-groups A program group bundles similar programs together so that only one of them runs per conversion. This page explains what they're for and when to use one. A program group bundles a set of similar [programs](/documentation/general/what-are-programs) together so that only one program in the group runs when a customer converts. Normally Siren tries to pay every program that matches a conversion. A program group overrides that default and forces the group to pick a single winner based on rules you configure. ## Why you'd want one The most common reason to reach for a program group is tiered commission rates. Imagine you want to pay your top affiliates a higher rate than the rest of your affiliates, so you run a Standard program and a Top program. A program only pays collaborators enrolled in it, so with each affiliate in exactly one tier and one referrer per sale, the right tier pays and nothing else does. You need the group when one sale can match both programs: an affiliate who is in both tiers while you promote them, a customer who clicked a Standard affiliate's link and later a Top affiliate's link, or a seasonal program running beside the base program. Without a group, both programs would pay on that sale. A program group solves this by letting you say "these two programs are really one program with two tiers, pick the one that applies." The [tiered affiliate program recipe](/recipes/tiered-affiliate-program) is a working example. The same pattern works for any situation where programs overlap and you need Siren to choose between them instead of running all of them. Seasonal promotions that temporarily replace a base program, VIP-only rates, and region-specific commission overrides all fit this shape. ## How the winner gets picked When a customer converts, the program group's structure decides which program in the group runs. See [Program Group Structures](/documentation/program-group-structures/choosing-a-program-group-structure) for the available options and how each one chooses a winner. Once a program group picks a winning program, that program runs as if it were the only one matched. It generates a conversion, the conversion follows the normal [approval flow](/documentation/general/what-is-a-conversion), and the resulting [obligation](/documentation/general/what-are-obligations) pays the collaborator tied to the winning engagement. ## When you don't need one Program groups are only useful when programs overlap. If your programs cover entirely different products, customer segments, or reward types, they can safely run side by side without a group. Siren is designed to pay every program it can, and reaching for a program group just because you have multiple programs is usually more friction than it's worth. Group programs together only when you actively need to prevent Siren from paying more than one of them. > **For developers:** This concept maps to the [`ProgramGroupConversionTriggered`](/documentation/developer-reference/events-conversions/program-group-conversion-triggered) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What Are Programs? Source: https://www.sirenaffiliates.com/documentation/general/what-are-programs A set of conditions that specify how and when collaborators earn rewards, along with the amounts of those rewards. import EventFlow from "@/components/content/EventFlow.astro"; A program is a set of conditions that specify how and when [collaborators](/documentation/general/what-is-a-collaborator) earn rewards, along with the size of those rewards. Programs are the most fundamental building block in Siren. Almost everything else exists to support them. A program describes what someone has to do to earn a payout and how that payout is calculated. Unlike [distributors](/documentation/general/what-are-distributors), which aggregate data and pay out on a schedule, programs are tied to a single [transaction](/documentation/general/what-are-transactions). A customer converts, and the program decides who gets credit and how much. You can build almost anything with a program as long as you can define three things: a measurable action that earns credit, a calculation for how much to pay, and a transaction to bind the payout to. ## Common program types An affiliate program pays a collaborator when someone they referred buys something. Try the [basic affiliate program](/recipes/basic-affiliate-program) recipe to get started. A customer loyalty program rewards an existing customer with store credit when they recommend a product to someone else who buys. A sales program pays a salesperson a commission when they close a deal. A royalty program pays a creator a percentage of sales on products they own or contributed to. The [product royalty program](/recipes/product-royalty-program) recipe shows a working example. These are just starting points. A single Siren install can run all four at once, and a single conversion can fire multiple programs. An affiliate refers a customer who buys a book, and Siren can pay both the affiliate and the author at the same time. ## The lifecycle of a program Each program moves through a structured pipeline from the first engagement to the final payout. ## Program structure Every program has a [program structure](/documentation/program-structures/choosing-a-program-structure) that decides who gets paid when a conversion happens. Some structures pick a single winner. Others split the reward among everyone who contributed to the customer's journey. ## Incentive structure Every program also has an [incentive structure](/documentation/incentive-structures/choosing-an-incentive-structure) that decides how much gets paid. Incentive structures use the [transaction](/documentation/general/what-are-transactions) data to calculate the payout, whether that's a percentage of revenue, a flat fee per sale, or a fixed amount per product. ## Engagement values Each program assigns a point value to the engagement types it tracks. When a collaborator triggers an engagement (a site visit through their referral link, a coupon code being used, a course completion) the engagement receives the point value configured for that type in the program. For programs where only one collaborator can win, these values don't matter. The collaborator either has an engagement or they don't. The values become important when the program structure needs to compare or weigh multiple collaborators' contributions. The [top score wins](/documentation/program-structures/top-score-wins) structure pays out to whichever collaborator has the highest score. The [performance weighted pool](/documentation/program-structures/performance-weighted-pool) divides the reward proportionally based on each collaborator's share of the total. In both cases, the values you assign decide how much each kind of engagement counts. For example, you might set a referral link click to 1 point and a coupon code use to 5 points. A coupon use would then outweigh five clicks in any score-based structure, signalling that you value coupon-driven conversions more highly. If you don't use score-based structures, the defaults work fine and you can ignore engagement values entirely. ## Cascading payouts across a hierarchy A program isn't limited to crediting the collaborator who triggered the engagement. Bind a program to a [collaborator group](/documentation/general/what-are-collaborator-groups) with a chain or tree structure, switch the engagement type's calculation method from Fixed to [Upline Cascade](/documentation/calculation-strategies/upline-cascade) or [Downline Cascade](/documentation/calculation-strategies/downline-cascade), and a single trigger fans out across the hierarchy. The triggering collaborator's upline (or downline) each earn at their configured per-layer rate, the cascade stops at the first zero layer you set, and inactive peers are skipped without consuming the slot. This is how Siren handles tiered commissions, sales overrides, and brokerage-style downlines. See [What is a cascade](/documentation/general/what-is-a-cascade) for the full mental model. > **For developers:** Siren is built on a typed event system. See the [events reference](/documentation/developer-reference/events-introduction) to understand how the framework processes programs through the attribution and payout pipeline. ## What Are Transactions? Source: https://www.sirenaffiliates.com/documentation/general/what-are-transactions A transaction is Siren's financial record of a purchase. This page explains what's stored on one, how transactions relate to conversions, and how line items drive commission calculations. A transaction is Siren's financial record of a purchase. It captures the full breakdown of what the customer bought and what they paid, including products, subscriptions, discounts, fees, shipping, and taxes. Every [conversion](/documentation/general/what-is-a-conversion) that results in a reward is tied back to a transaction, and the transaction is where Siren finds the numbers it needs to calculate what a collaborator is owed. ## How transactions get into Siren Most transactions are created automatically. When a customer completes a purchase in WooCommerce, Easy Digital Downloads, LifterLMS, or NorthCommerce, the active integration pushes the order into Siren as a transaction. The integration copies over every line item, applied coupon, shipping charge, and tax line so Siren has a complete picture of the sale. You can also create transactions by hand. If a sale happened outside your normal commerce flow (a phone order, an invoice paid by wire, or a refund correction), the Transactions screen in the Siren admin lets you build a transaction manually and attach line items to it. See [Creating Transactions Manually](/documentation/getting-started/creating-transactions-manually) for the full workflow. ## One transaction, many conversions A single transaction can be linked to multiple conversions. This happens whenever more than one program has a reason to pay out on the same purchase. An affiliate who referred the customer earns a commission through an affiliate program. The author of the book the customer bought earns a royalty through a separate royalty program. Both conversions point at the same transaction and pull their reward calculations from the same underlying data. When a transaction is reversed through a [refund](/documentation/general/how-refunds-work), every conversion tied to it is rejected automatically, and any unpaid obligations that came from those conversions are rejected along with them. ## Line items and why their types matter Everything on a transaction is stored as a line item. Each line item has a type, and the type tells Siren what kind of financial data it represents. Products and subscriptions are the items being sold. Fees are extra charges like signup fees or setup fees. Discounts are coupons or promotions that reduced the total. Shipping and tax are exactly what they sound like. Siren categorizes line items this way because commission calculations almost always care about the distinction. Most programs pay on the product subtotal after discounts, and exclude shipping and tax because those don't represent revenue the business actually keeps. A royalty program might only count line items for products the collaborator owns. A subscription program might only count subscription line items and ignore one-time products in the same cart. The rules for which line items count are configured on each program and [distributor](/documentation/general/what-are-distributors) through transaction compilers and filters. See [Transaction Filtering](/documentation/general/line-item-filters) for how to set up which parts of a transaction drive commission, and which parts are ignored. > **For developers:** This concept maps to the [`TransactionCreated`](/documentation/developer-reference/events-payments/transaction-created) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What is a cascade? Source: https://www.sirenaffiliates.com/documentation/general/what-is-a-cascade A payout that spreads across multiple collaborators per single trigger, walking up or down a chain. A cascade is what happens when a single trigger (one sale, one referral, one form submission) pays out to more than just the collaborator who caused it. Instead of crediting only that person, Siren walks the [collaborator group](/documentation/general/what-are-collaborator-groups) they belong to and emits a credit at each layer above (or below) them. The triggering collaborator's manager earns something. Their manager's manager earns something smaller. And so on, until the cascade hits a layer you've set to zero. This is the "tiered commissions" or "override commissions" model that sales orgs and brokerage networks rely on. Siren's framing is more general, covering any per-trigger fan-out across a hierarchy, but if you've been searching for "override commissions" or "tiered commissions," cascades are the feature you're looking for. ## How it works Cascades depend on three things working together: a hierarchical collaborator group, a cascade [calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy), and per-layer point values. First, you bind a [program](/documentation/general/what-are-programs) or [distributor](/documentation/general/what-are-distributors) to a collaborator group that has structure. A [flat group](/documentation/collaborator-group-structures/flat) won't work, because there are no layers to walk. You need a [linear chain](/documentation/collaborator-group-structures/linear-chain) (ordered by position) or a [parent-child](/documentation/collaborator-group-structures/parent-child) tree. Then you pick a cascade calculation strategy. Two ship today: [upline cascade](/documentation/calculation-strategies/upline-cascade) walks toward the top of the chain (people above the triggering collaborator), and [downline cascade](/documentation/calculation-strategies/downline-cascade) walks toward the bottom (people below them). The picker on the program or distributor edit screen hides cascade strategies when the bound group is flat, because there's nothing for them to walk. Finally, you set per-layer point values: `pointsAtLayer1` through `pointsAtLayer5`. Layer 1 is the nearest neighbor in the chosen direction, one step up for upline, one step down for downline. Layer 2 is two steps away. The cap is five layers. When a trigger fires, the cascade walks the group from the triggering collaborator outward. At each layer, it emits one credit per collaborator at that layer, using the points you configured. A layer set to 0 (or left unset) stops the cascade, and nothing past that layer gets credited. That's the lever you use to say "only pay three deep." Two behaviors are worth pinning down. The triggering collaborator is never credited by the cascade. Their direct payout comes from a separate calc strategy (usually [fixed](/documentation/calculation-strategies/fixed)) bound to the same program, and the cascade only handles the people around them. Inactive peers (suspended or deleted) are skipped and earn nothing, but the cascade does not renumber the layers around them. A suspended person leaves their layer unpaid, and everyone else keeps their own layer and rate. So if the person at layer 1 is suspended, layer 1 pays nothing, and the next person up stays at layer 2 and earns the layer-2 rate. They are not promoted into the empty slot. In a [parent-child](/documentation/collaborator-group-structures/parent-child) tree, where a layer can hold several peers, one suspended peer does not stop the others. The remaining active peers at that layer still earn the per-layer rate. ## A concrete example Picture a four-person linear chain: chain-one at position 1, chain-two at position 2, chain-three at position 3, chain-four at position 4. Lower position numbers are higher in the chain, so chain-one is at the top and chain-four is at the bottom. You bind a program to this group with an upline cascade and the following per-layer points: - `pointsAtLayer1: 100` - `pointsAtLayer2: 50` - `pointsAtLayer3: 25` - `pointsAtLayer4: 0` chain-four makes a sale. Here's what the cascade emits: - chain-three earns 100 points (layer 1, one step up from chain-four) - chain-two earns 50 points (layer 2) - chain-one earns 25 points (layer 3) - chain-four earns nothing from the cascade If chain-three had instead made the sale, the cascade would only have credited chain-two (100) and chain-one (50). The cascade stops when it runs out of collaborators above the seller. Now flip the direction. Same group, but the program uses a downline cascade with the same per-layer points. chain-one makes a sale. The cascade walks down: - chain-two earns 100 points (layer 1, one step down) - chain-three earns 50 points (layer 2) - chain-four earns 25 points (layer 3) - chain-one earns nothing from the cascade The mechanic is symmetric. Direction is just a setting. ## Cascades and pools The per-layer numbers are scores, not dollars. They feed into whatever incentive structure the program (or distributor) uses to turn scores into payouts. With a fixed-per-incentive structure, each layer's points become a literal dollar payout, so 100 points might mean $100, depending on how you've set up the math. The scores act as the payout amounts directly. With a [performance-weighted pool](/documentation/distribution-structures/performance-weighted-pool), the scores become weights for splitting a fixed pool. The 100/50/25 from the example above becomes a 4:2:1 split, where chain-three gets four shares of the pool, chain-two gets two, chain-one gets one. If the pool is $700, that's $400, $200, and $100. The point is that cascade scores are a layer in the calculation, not the final answer. The program's distribution structure decides what to do with them. ## When to use a cascade Reach for a cascade when a single trigger should fan out across several people rather than crediting only the person who caused it, and when the reward depends on each person's distance from the trigger. The classic cases are multi-tier sales organizations, where a rep makes a sale and their manager and regional director each take a smaller override. Brokerage and agency override structures follow the same shape, with senior real estate and insurance brokers earning on the production of the agents they oversee. Channel and partner hierarchies fit too, where a partner manager earns an override on the partners they manage. Management and mentor overrides, where a team lead earns a slice of every sale someone on their team makes, and team-based commissions that tie a leader's pay to their team's results round out the family. If you only want to credit the person who caused the trigger, you don't need a cascade. A fixed calc on a flat group is the simpler tool. Cascades are specifically for the fan-out cases. > **For developers:** A cascade is a calculation strategy that walks a collaborator group's structure and emits one credit per layer. See the [custom calculation strategies](/documentation/extensions/calculation-strategies) extension page for the full API, and the [collaborator group events reference](/documentation/developer-reference/events-collaborator-groups) for the binding and structure events a cascade depends on. ## What is a Collaborator? Source: https://www.sirenaffiliates.com/documentation/general/what-is-a-collaborator Individuals or entities such as bloggers, influencers, or businesses that participate in your programs to promote your products or services. A collaborator is anyone who participates in one of your [programs](/documentation/general/what-are-programs) and earns rewards for measurable actions. You might think of these as "affiliates," but in Siren an affiliate is just one of many shapes a collaborator can take. Collaborators can be influencers driving traffic from social media, authors creating content you sell, instructors building courses on your platform, salespeople closing deals, or businesses cross-promoting your products to their own customers. The unifying thread is that each one performs a measurable action that contributes to your business, and Siren tracks those actions through [engagements](/documentation/general/what-is-an-engagement) so the right people get credit. A collaborator isn't locked into one role. The same person can belong to multiple programs and earn from all of them. An instructor on a course platform might earn an affiliate commission for referring students, a royalty on sales of their own courses, a profit share based on how often their courses are watched, and a bonus when students complete their material. Each program rewards a different aspect of their contribution. The [online course platform starter](/recipes/online-course-platform-starter) recipe sets up exactly this kind of multi-program arrangement. This flexibility is intentional. People contribute in different ways, and Siren is built to capture as many of those contribution patterns as possible without forcing everyone into the same mold. ## Tracking collaborator performance A collaborator's performance is tracked through [engagements](/documentation/general/what-is-an-engagement), which are recorded whenever an action they're responsible for happens on your site. For Siren to attribute the action correctly, it needs some way to tie that action back to the collaborator. There are two main mechanisms. ### Tracking IDs The most common method is a tracking ID, a short string of characters appended to URLs the collaborator shares. When someone visits your site through a link containing that ID, Siren knows who referred them. Tracking IDs can be changed at any time. Old IDs stay valid even after you assign a new one, so existing referral links don't break. A collaborator with the ID "ABC" who later switches to "DEF" will still earn from anyone clicking an old ABC link, but they'll only see DEF in their dashboard going forward. Both IDs are stored as [aliases](/documentation/resource-reference/aliases), which preserve a full history of every code ever assigned. ### Coupon codes Some integrations support coupon code tracking. You assign a coupon to a collaborator, and when a customer applies that code at checkout, Siren creates an engagement for the owner. This is useful for channels where a clickable link isn't practical, like podcast sponsorships or printed materials. See [Coupon Code Tracking](/documentation/general/coupon-tracking) for the full setup. > **For developers:** Siren is built on a typed event system. See the [events reference](/documentation/developer-reference/events-introduction) to understand how the framework processes collaborator attribution through the event pipeline. ## What is a Conversion? Source: https://www.sirenaffiliates.com/documentation/general/what-is-a-conversion A conversion is the moment Siren credits a collaborator for a customer action. This page explains how they're created, what the statuses mean, and how they relate to transactions. import EventFlow from "@/components/content/EventFlow.astro"; A conversion is Siren's record that a [collaborator](/documentation/general/what-is-a-collaborator) influenced a customer action worth rewarding. That action might be a purchase, a subscription renewal, or a lead signup, depending on how the [program](/documentation/general/what-are-programs) is configured. Sale conversions are available in all tiers. Lead and renewal conversions require Siren Essentials. Every conversion ties one collaborator to one triggering event inside one program. A single customer action can create several conversions at once if multiple programs match, which is how the same purchase can reward an affiliate and a royalty-earning author at the same time. ## How a conversion gets created ## Conversion statuses Every conversion carries one of three statuses. A *pending* conversion is waiting for review before it can generate an [obligation](/documentation/general/what-are-obligations). An *approved* conversion has been validated and counts toward what the collaborator is owed. A *rejected* conversion has been deemed invalid, either manually or automatically (for example, when a [refund](/documentation/general/how-refunds-work) comes through), and will not generate a payout. You can configure programs to auto-approve conversions or to hold them in pending until you review them. Holding in pending is useful when you want a buffer to catch fraud or refunds before they turn into obligations. ## Conversions versus transactions A conversion records the credit. A [transaction](/documentation/general/what-are-transactions) records the money. The two are linked but distinct. When a customer buys a book through an affiliate link and that same book pays a royalty to its author, one transaction is created for the purchase and two conversions are created against it, one for the affiliate and one for the author. Each conversion drives its own obligation under its own program, but they both draw their financial details from the same underlying transaction. Transactions carry the itemized breakdown (products, discounts, shipping, tax, fees), and conversions use that breakdown to calculate the reward. If a program excludes tax and shipping from its commission base, the conversion applies those rules against the transaction it's linked to. ## Manual conversions When a sale happens outside your normal tracking (a phone order, an in-person sale, or a case where tracking failed), you can create a conversion by hand. See [Manually Attribute a Transaction](/documentation/getting-started/manually-attribute-a-transaction) for the workflow. > **For developers:** This concept maps to the [`ConversionsAwarded`](/documentation/developer-reference/events-conversions/conversions-awarded) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What is a Fulfillment? Source: https://www.sirenaffiliates.com/documentation/general/what-is-a-fulfillment A fulfillment is how obligations turn into payouts. This page explains how fulfillments are created, how they produce payout records, and how to work through them. import EventFlow from "@/components/content/EventFlow.astro"; A fulfillment is the step that turns [obligations](/documentation/general/what-are-obligations) into actual payouts. When you decide it's time to pay your collaborators, you create a fulfillment, Siren gathers up the obligations you want to settle, and the result is a set of payout records ready to be processed. ## How a fulfillment is built ## Processing the payouts Once payout records exist, you decide how to move the money. The most common approach is to export the payout list to CSV, run that file through your payment platform of choice, and then mark the payouts as paid in bulk. For smaller programs, or for handling one-off exceptions, you can also mark individual payout records as paid manually as each transfer goes out. Either way, the payout record is the thing you update when the money lands. Siren doesn't push money on your behalf. It gives you the totals, tracks which payouts have been settled, and leaves the actual disbursement to whatever tool you already use. ## Reviewing historical fulfillments Every fulfillment becomes a permanent ledger entry. You can open an old fulfillment and see exactly which obligations were included, which collaborators got paid, how much each one received, and when. This is what you reach for during audits, dispute resolution, or when a collaborator asks why last month's payout looked the way it did. The fulfillment's [activity feed](/documentation/general/activity-feeds) shows each payout that was cut, each obligation that was rolled into it, and any status changes along the way, so the full story is on one screen instead of spread across several. It's also what protects you if a refund comes in after a payout has been sent, since the completed obligations on a historical fulfillment are deliberately left untouched by the refund pipeline. > **For developers:** This concept maps to the [`FulfillmentCreated`](/documentation/developer-reference/events-payments/fulfillment-created) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What Is an Affiliate Program? Source: https://www.sirenaffiliates.com/documentation/general/what-is-an-affiliate-program An affiliate program is a business arrangement where a company pays someone a commission for driving sales to their website. import EventFlow from "@/components/content/EventFlow.astro"; An affiliate program is a business arrangement where a company pays someone a commission for driving sales to its website. Instead of buying ads, you recruit people who already believe in your product and pay them only when their efforts produce a sale. It's a pay-for-performance model that turns your network into a distributed sales force. In Siren, an affiliate program is one specific shape of a more general concept called a [program](/documentation/general/what-are-programs). A program defines what someone has to do to earn a reward and how much they earn. An affiliate program is the version where the action is "refer a customer who buys something" and the reward is a commission on that sale. ## How it works Affiliates promote your products through their own channels (blogs, social media, newsletters, podcasts) using a unique referral link or coupon code. When one of their referrals makes a purchase, Siren attributes the sale and records what you owe. The whole pipeline runs automatically. Your only manual touch points are reviewing conversions and triggering the payout when you're ready. ## Why it works Traditional advertising costs money whether or not it produces sales. An affiliate program flips that. You pay nothing until a referral converts, and the commission comes out of revenue you wouldn't have earned otherwise. It scales with your collaborators' effort, not your ad budget. It also taps into trust. When a creator recommends a product to their audience, that recommendation carries more weight than a banner ad ever will. ## Setting one up The fastest way to stand up your first affiliate program in Siren is to start from a working template. The [basic affiliate program](/recipes/basic-affiliate-program) recipe creates a percentage-based commission program with sensible defaults that you can tune from there. If you want to dig into the underlying pieces first, read [What Are Programs?](/documentation/general/what-are-programs) for the full breakdown of how programs are structured. > **For developers:** Siren is built on a typed event system. See the [events reference](/documentation/developer-reference/events-introduction) to understand how the framework processes the concepts described here. ## What is an Engagement? Source: https://www.sirenaffiliates.com/documentation/general/what-is-an-engagement Any track-able event where a potential customer's action is tied to a collaborator. import EventFlow from "@/components/content/EventFlow.astro"; An engagement is any trackable event that ties a potential customer's action to a [collaborator](/documentation/general/what-is-a-collaborator). The classic example is a visitor arriving at your site through an affiliate link, but engagements cover a much wider range of interactions: an existing customer watching a course lesson made by an instructor, a reader landing on a blog post written by a partner, a coupon code being applied at checkout. If a measurable thing happens on your site and you can tie it to a collaborator, Siren can record it as an engagement. Engagements are the connective tissue between customer behavior and collaborator credit. Without them, [conversions](/documentation/general/what-is-a-conversion) would have nowhere to attach. ## Example: a site visit ## Customers can trigger many engagements A customer's journey can include many engagements with different collaborators or repeated interactions with the same one. Each engagement carries the point value defined by the [program](/documentation/general/what-are-programs) it belongs to. A visitor might click an affiliate's link, return a week later through a different collaborator's coupon code, then finally check out. By the time they convert, multiple collaborators have engagements on file, and the program structure decides which ones win. ### A multi-engagement journey This is what makes multi-touch attribution possible. If you want every collaborator who contributed along the way to share in the reward, see the [multi-touch sales attribution](/recipes/multi-touch-sales-attribution) recipe. ## Engagement triggers Different programs use different engagement triggers depending on what behavior you want to reward. Site visits and coupon usage are the most common, but Siren also supports course completions, lesson completions, blog post visits, form submissions, and [manual attribution](/documentation/general/manual-attribution) for cases where you need to assign credit by hand. See [Site Visited](/documentation/general/site-visited) and the related trigger pages for the full list. ## One trigger, one engagement, or many By default, one trigger creates one engagement on one collaborator, the person who caused it. Each engagement type on a program can carry a [calculation strategy](/documentation/calculation-strategies/choosing-a-calculation-strategy) that changes this. Fixed is the default and behaves the way the rest of this page describes. The cascade strategies ([Upline Cascade](/documentation/calculation-strategies/upline-cascade), [Downline Cascade](/documentation/calculation-strategies/downline-cascade)) fan a single trigger out across the bound [collaborator group](/documentation/general/what-are-collaborator-groups), creating one engagement per layer at the per-layer score you configure. > **For developers:** This concept maps to the [`EngagementsTriggered`](/documentation/developer-reference/events-attribution/engagements-triggered) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What is an Opportunity? Source: https://www.sirenaffiliates.com/documentation/general/what-is-an-opportunity A single instance of something from which rewards can be issued. Usually represents a site visitor, but can encompass other scenarios. import EventFlow from "@/components/content/EventFlow.astro"; An opportunity is Siren's record of a single potential reward-generating subject, usually a site visitor. It's the anchor that ties [engagements](/documentation/general/what-is-an-engagement) to a person so that when they convert, Siren knows which [collaborators](/documentation/general/what-is-a-collaborator) to credit. If you're coming from another affiliate system, you might think of an opportunity as a "visit." In Siren, a visit is just one kind of opportunity. Opportunities can also represent sales leads, support tickets, or any other trackable subject that might lead to a reward later. Opportunities are mostly invisible. You almost never interact with them directly. Siren creates and resolves them automatically as visitors move through your site, and the rest of the system uses them under the hood to keep attribution correct. ## How site visits create opportunities The most common way an opportunity gets created is when someone visits your site. ## Opportunity resolution Sometimes a visitor ends up with two opportunity IDs. This usually happens when a logged-out visitor browses around (creating an anonymous opportunity tied to their cookie) and then logs into an account that already had an opportunity from a previous session. When Siren detects the overlap, it merges the two IDs together. The result is a single, continuous opportunity that captures the visitor's full history across sessions and devices. > **For developers:** This concept maps to the [`OpportunityTriggered`](/documentation/developer-reference/events-attribution/opportunity-triggered) event. See the [events reference](/documentation/developer-reference/events-introduction) for the full pipeline. ## What is Beacon? Source: https://www.sirenaffiliates.com/documentation/getting-started/what-is-beacon Beacon is Siren's free AI assistant. It helps you design incentive programs, explore recipes, and get answers about Siren's features. Beacon is Siren's free AI assistant. It has deep knowledge of Siren's documentation, blog posts, recipes, podcast transcripts, and integration guides. You can use it to design incentive programs, explore configuration options, get answers about how features work, and generate ready-to-install recipes for your Siren installation. Beacon is free for everyone. You don't need a Siren license or purchase to use it. ## The fastest way to try Beacon The [Siren Affiliates Beacon custom GPT](https://chatgpt.com/g/g-69d6578e07bc8191b5e0820b489f6446-siren-affiliates-beacon) on ChatGPT is the easiest way to start. It works immediately with no setup. Just open the link, ask a question about Siren, and Beacon will search its knowledge base to give you an informed answer. You can ask it things like "What's the best way to set up a tiered affiliate program?" or "Create a recipe for a course creator royalty program" and it will pull from Siren's actual documentation and recipe library to help you. ## For developers: the MCP server Beacon is also available as an MCP (Model Context Protocol) server that you can connect to Claude, ChatGPT, VS Code, JetBrains IDEs, Cursor, and any other MCP-compatible client. The MCP integration gives your AI tools direct access to Beacon's search, knowledge retrieval, and recipe creation capabilities. If you want to connect Beacon to your development environment or AI workflow, see the [Beacon MCP setup guides](/documentation/beacon/introduction) in the Developer section. ## What is Siren? Source: https://www.sirenaffiliates.com/documentation/getting-started/what-is-siren-the-comprehensive-affiliate-management-platform Learn exactly what Siren is, what it does, and how it helps your business grow. import Transcript from "@/components/media/Transcript.astro"; ## What does Siren do? Siren is an incentive program builder that runs on WordPress. Most people find it while searching for an affiliate plugin, but Siren handles far more than traditional affiliate programs. You define the rules for how people earn rewards, and Siren tracks the activity, calculates what's owed, and manages payouts. ## What kinds of programs can you build? Most people come to Siren for a standard affiliate program: pay a commission when a partner drives a sale. That's the most common shape, and Siren does it well. But the same engine handles a lot more. Course platforms use Siren to pay instructors a share of the revenue their courses generate. Marketplaces like Etsy-style art and print-on-demand sites pay vendors royalties whenever their products sell. SaaS companies run partner programs that pay recurring commissions to resellers and integration partners. And plenty of businesses use Siren internally, running sales team SPIFs and per-sale bonuses for their own employees. The common thread is pay for performance, not hours. ## How does Siren track and pay collaborators? Siren is built around a handful of core concepts that connect to form a complete incentive system: programs, collaborators, engagements, conversions, obligations, and distributors. For a full walkthrough of how they fit together, see the [User Guide concepts](/documentation/general/what-are-programs). ## How are reward amounts calculated? Siren supports percentage-based, fixed-per-transaction, and fixed-per-product rewards, and the right choice depends on what you're incentivizing. See [Choosing an Incentive Structure](/documentation/incentive-structures/choosing-an-incentive-structure) for help picking one. ## Which plugins does Siren work with? Siren works with WooCommerce, Easy Digital Downloads, NorthCommerce, LifterLMS, LearnDash, and Gravity Forms. Each integration plugs the same core system into a different commerce or engagement platform, so your programs behave consistently regardless of which plugin runs the storefront. See the [integration feature matrix](/documentation/general/integration-feature-matrix) for what each integration supports. ## Ready to build something? Head to the [Quick Start](/documentation/getting-started/quick-start) and set up your first program. Siren is a toolkit for building incentive-driven systems where you pay for performance, not hours. It powers affiliate programs, artist royalties, co-founder profit shares, subcontractor marketplaces, and sales bonuses, all from a single WordPress plugin. ## Why a cascade pays no one Source: https://www.sirenaffiliates.com/documentation/calculation-strategies/cascade-troubleshooting How a cascade fails closed by emitting no credits, and the checklist to follow when it pays nobody. A cascade can fire and credit nobody. When that happens it is almost always because the bound [collaborator group](/documentation/general/what-are-collaborator-groups) cannot give the cascade any layers to walk. Siren handles that case by failing closed. It emits no credits, returns an empty result, and in some cases logs the reason. This page explains why that happens and gives you a checklist to follow when a cascade pays no one. ## Fail closed means no credits, not a fallback When a cascade cannot do its job, it returns nothing. It does not quietly switch to [Fixed](/documentation/calculation-strategies/fixed) and credit the triggering collaborator instead. There is no Fixed fallback at runtime. Every path that cannot produce a valid cascade returns an empty set of credits, so the trigger results in zero engagement or metric rows from that calculation. This is deliberate. A cascade that cannot walk a chain has no defensible answer for who to pay, so it pays no one rather than guessing. The cost is that a misbound cascade is silent. The work to find out why is on this page. ## The picker filter and runtime fail-closed are two different things There are two separate guards, and they do not do the same job. The picker filter is a frontend convenience. On the Programs Edit and Distributors Edit screens, the calculation picker hides cascade options when the bound collaborator group cannot provide layers. A flat group provides no layered walker, so the picker does not offer Upline Cascade or Downline Cascade while a flat group is bound. This is a presentation filter only. It keeps you from selecting an option that could not work. See [Why some calculation methods disappear](/documentation/general/calc-capability-matching) for how that matching works. Runtime fail-closed is the actual safety net. The picker hides the option in the UI, but it does not rewrite a calculation that is already saved, and the API does not coerce a cascade into anything else. If a program or distributor ends up with a cascade calculation bound to a group that cannot supply layers, the calculation still runs when a trigger fires. At that point it fails closed and emits no credits. So a cascade can pay no one even though the picker would never have offered it, because the picker only governs what you can pick today, not what is already stored. ## What causes a cascade to emit nothing Every case below makes the cascade return an empty set of credits. The triggering collaborator is never credited by a cascade, so failing closed means nobody is paid by that calculation for that trigger. ### The bound group is flat or lacks a layered walker A cascade needs a layered walker to know who sits at each layer. A flat group does not provide one. Its directional walkers are empty, so the cascade walks nothing and returns no credits. This is the most common cause. A flat group fails quietly. Because its walker is empty, the cascade simply has nothing to iterate and returns an empty set without logging. A custom or hierarchical structure that yields walker steps without layer information is treated as a misconfiguration. In that case the cascade returns no credits and logs a `RuntimeException` so the silent no-op is traceable. Either way the result is the same: no credits. ### The group is deleted or unbound A cascade reads which group is bound from the program config (engagement side) or the distributor config (metric side). Several states here produce no credits: - No group is bound at all (the binding is absent or zero). The cascade returns nothing. - The binding points at a group that has since been deleted. The lookup fails and the cascade returns nothing. - The bound group's structure resolver cannot be found. The cascade returns nothing. ### The triggering collaborator is not a member of the bound group A cascade walks the chain starting from the triggering collaborator's position inside the bound group. If the collaborator who fired the trigger is not a member of that group, there is no position to start from, so the cascade returns no credits. ### The triggering collaborator is at the end of the chain A cascade walks away from the triggering collaborator, so it needs someone on the side it walks toward. An Upline Cascade started by the collaborator at the top of the chain has no one above to credit. A Downline Cascade started by the collaborator at the very bottom has no one below. The triggering collaborator is a valid member with valid layers, but there is no one in the walked direction, and the cascade never credits the collaborator who fired the trigger, so the result is empty. A sale or metric from the person at the end of the chain pays no one. ### Layer 1 is set to 0 or unset Per-layer scores are read from `pointsAtLayer1` through `pointsAtLayer5`. A value of 0 or less at a layer terminates the cascade at that point, so only `pointsAtLayer1` can empty the whole cascade. If `pointsAtLayer1` is 0 or unset, the cascade stops before it credits anyone and pays nobody. A 0 at a later layer is not a failure. It stops the cascade there and the layers before it are still paid, which is how operators signal "no further layers". If you meant the cascade to pay and it pays no one, check that layer 1 has a positive value. ## Checklist when a cascade pays no one Work through these in order. Each one maps to a cause above. The checklist assumes the trigger fired and the cascade ran. If you are not sure the underlying event happened at all, the sale or metric that should start the cascade, confirm that first, because a cascade only pays out when something triggers it. 1. Confirm a group is actually bound. Open the program (or distributor) and check that a collaborator group is bound to it. An unbound calculation emits nothing. 2. Confirm the bound group still exists. If the group was deleted after it was bound, the binding points at nothing and the cascade returns no credits. 3. Check the group's structure. A [flat](/documentation/collaborator-group-structures/flat) group cannot supply layers, so a cascade bound to it pays no one. Switch the group to a [linear chain](/documentation/collaborator-group-structures/linear-chain) or [parent-child](/documentation/collaborator-group-structures/parent-child) structure, or bind a group that already uses one. The [create a collaborator group](/documentation/getting-started/create-a-collaborator-group) guide covers where the structure is set. 4. Confirm the triggering collaborator is a member of the bound group. If the person who fired the trigger is not in the group, the cascade has no position to start from. 5. Confirm there is someone in the direction the cascade walks. An Upline Cascade needs at least one collaborator ranked above the one who fired the trigger, and a Downline Cascade needs at least one below. A trigger from the end of the chain leaves the cascade no one to credit. 6. Check the per-layer values. Make sure `pointsAtLayer1` is a positive integer. A 0 or unset layer 1 stops the cascade before it credits anyone. Confirm that the layers you intend to pay are above 0 and that any 0 is where you actually want the cascade to stop. 7. Check the active status of the peers. A cascade skips inactive peers without consuming the layer slot, so if every peer along the walk is inactive, the cascade can finish having credited no one even though it ran correctly. If you reach the end of the list and the calculation is still bound to a hierarchical group with a member trigger and positive layers, check your server log for a `RuntimeException` mentioning a walker step without layer information. That points at a structure that yields walker steps the cascade cannot read, which is a misconfiguration of the structure rather than of the calculation. For the underlying mechanics, see [Upline Cascade](/documentation/calculation-strategies/upline-cascade) and [Downline Cascade](/documentation/calculation-strategies/downline-cascade). For how the picker decides which calculations to offer, see [Why some calculation methods disappear](/documentation/general/calc-capability-matching). ## Why rates live on programs, not collaborators Source: https://www.sirenaffiliates.com/documentation/general/why-rates-on-programs Siren attaches commission rates to programs, not to individual collaborators, by design. This page explains the reasoning, the operational tradeoffs, and the tier patterns that solve the use case people usually mean when they ask for per-collaborator rates. Siren doesn't let you set a commission rate on an individual collaborator. You can't open an affiliate's profile and type "25%" into a rate field. That's a deliberate design choice, and it surprises people coming from platforms where per-affiliate overrides are the default way to handle "some of my affiliates earn more than others." The question is almost always reasonable. You probably do have a few affiliates who earn more, either because they perform better, because they negotiated a better deal, or because they're friends. The problem isn't the use case. The problem is that per-collaborator rate fields are a low-quality tool for solving it. This page explains what Siren does instead and why. If you'd rather read the underlying philosophy in a narrative form, the blog post at [how multiple programs make Siren easier to maintain](/blog/how-multiple-programs-makes-siren-easier-to-maintain) walks through the same reasoning with examples. This page is the shorter, more technical version you can link collaborators to when they ask. ## The case against per-collaborator rate overrides There are three specific reasons Siren avoids this pattern. ### Audit trail clarity When rates live on [programs](/documentation/general/what-are-programs), the question "what deal is this collaborator on?" has a simple answer. You look at which programs they're enrolled in, you read the rate on each program, you're done. The programs list itself is the source of truth. With per-collaborator overrides, the same question requires three lookups. First you check the override field on the collaborator. Then you check the program rate. Then you check which of those two wins, which depends on whatever priority rule the platform uses. Multiply that across 200 affiliates and the audit picture becomes opaque. You can't look at a single screen and understand who earns what. You have to cross-reference fields that could diverge at any time. ### Change safety Editing a program rate updates everyone on that program at once, predictably. If you change the rate from 20% to 22%, every future conversion on that program calculates at 22%. There's no ambiguity. Editing one collaborator's rate override silently creates drift between collaborators who are supposedly on the "same" program. The next person who opens the program and sees 20% has no idea that some collaborators are actually earning 22%, 25%, or a flat rate because someone negotiated with them months ago. The program rate is no longer the source of truth, but nothing on the program screen tells you that. This is how programs slowly decay into the spaghetti of "special cases" that make affiliate tooling so painful to maintain. ### Pattern enforcement If you find yourself wanting to set rates one collaborator at a time, you almost always have a tier pattern hiding underneath. Your top three performers. Your strategic partners. Your friends and family. Your beta group. Whatever shape the pattern takes, it's usually not "50 bespoke one-off rates with no structure." It's "three or four tiers that I've been implicitly carrying in my head." Forcing the discipline of creating a program for each tier surfaces that pattern and makes it explicit. You end up with a "Standard Affiliate" program and a "Top Affiliate" program instead of a spreadsheet of custom rates you have to remember. That's the shape that stays maintainable a year from now. ## The patterns that replace per-collaborator rates Siren has three concrete answers for the use case you probably actually have. ### Tiered programs, one per rate This is the most common answer. Create a "Standard Affiliate" program at 15% and a "Top Affiliate" program at 25%, and enroll each collaborator in the tier they belong to. A program only records referrals for collaborators enrolled in it, so a Standard affiliate's sale pays the Standard rate and a Top affiliate's sale pays the Top rate. Two programs are all you need, and this works on every tier, including Lite. Two plain programs share one gap: a sale that matches both of them. That happens when a customer clicked a Standard affiliate's link and later a Top affiliate's link, or when someone sits in both tiers while you move them up. Each program pays its own referral, so that sale pays two commissions. On Lite, keep the overlap small: when you promote someone, remove them from Standard before you add them to Top. On Essentials and above, put both programs in a [program group](/documentation/general/what-are-program-groups) and the group picks one winner for any sale that matches both. The [tiered affiliate program recipe](/recipes/tiered-affiliate-program) installs this pattern with the program group included, so the recipe itself needs Essentials. The [AffiliateWP migration guide](/documentation/migration/affiliatewp) walks through converting a per-affiliate override structure into this shape. ### Bespoke deals as their own programs If you've negotiated a one-off rate for a strategic partner, create a program named "Strategic Partner: Acme Corp" at the agreed rate and enroll only Acme in it. That might feel like overkill for a single collaborator, but it's the right shape. The deal is written down in an auditable place, the rate is visible in the programs list, and if you renegotiate later you update one program instead of hunting for a hidden override field. The friction of naming the deal explicitly is the right friction. If you're constantly creating bespoke programs for individual partners, that's a signal that your rate structure is under pressure and you should probably consolidate, not that the tool is getting in your way. ### Performance-weighted distributors for variable bonuses If the variable amount depends on aggregate performance rather than a pre-negotiated rate (top affiliate of the month gets a bonus, contributors to a campaign split a pool, monthly leaderboard prizes), don't try to encode that logic into per-collaborator rates at all. Use a [distributor](/documentation/general/what-are-distributors) instead. Distributors run on a schedule, track metrics over a period, and distribute a reward pool based on the structure you pick. Top Score Wins gives the whole pool to the best performer. Performance Weighted Pool splits it proportionally. Shared Engagement Pool splits it equally among everyone who participated. These patterns are what people usually mean when they say "I want top performers to earn more," and they scale cleanly without touching individual rates. ## Engaging the 200-affiliate criticism The honest pushback on all of this sounds like: "That's fine for a new program, but I have 200 existing affiliates with custom rates I've been negotiating for years. Converting them to tiered programs is a huge amount of audit work." That's true. Migrating an existing program with lots of historical custom rates does mean doing the audit work of grouping people into tiers. You'll probably discover that your 200 affiliates actually fall into four or five rate buckets when you look closely, plus a handful of genuine one-offs. That audit is real work, and we're not going to pretend it isn't. The payoff is that after the migration, you have a tiered structure that's auditable and changeable in one place, instead of 200 individual rate fields that could drift from each other at any time. The pain is front-loaded. The maintenance cost afterward is dramatically lower. If your current structure works well enough and you don't want to do the audit, staying on your current tool is a legitimate choice. Siren's design is optimized for the long tail, not for a zero-cost migration. ## When you'd want this anyway There's a small set of cases where per-collaborator rate overrides would genuinely be better than Siren's model. The most honest example is a program with hundreds of bespoke negotiated deals where no tier pattern exists, each rate was truly individual, and there's no way to consolidate them without losing the business relationships. If that's your situation, Siren isn't the right tool for you today. We're not going to pretend it is. For most programs, though, the per-collaborator rate instinct comes from the habits of legacy tooling rather than from the underlying problem. The patterns on this page cover the vast majority of real cases. If you're still not sure your use case fits, the blog post at [how multiple programs make Siren easier to maintain](/blog/how-multiple-programs-makes-siren-easier-to-maintain) has more detail on the underlying philosophy and a few worked examples that might match your situation. ## Why some calculation methods disappear Source: https://www.sirenaffiliates.com/documentation/general/calc-capability-matching How Siren filters the calculation-strategy picker based on what the bound collaborator group can support. You're editing a Program or a Distributor. You remember picking Upline Cascade for it last week. Today the calculation-strategy dropdown only shows Fixed. Upline Cascade and Downline Cascade are gone. This isn't a bug. Siren hid them on purpose, because the [collaborator group](/documentation/general/what-are-collaborator-groups) currently bound to this Program or Distributor can't support a cascade. ## What's happening Every calculation strategy declares the capabilities it needs from the group it'll walk. Today there's one such capability: `hasLayer`. A calc that requires `hasLayer` is saying "I need to know which layer of the group I'm at while I walk it." Upline Cascade and Downline Cascade both require it. Fixed doesn't require anything. It just hands out a flat amount and doesn't care about structure. On the other side, every [collaborator group structure](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) declares what it provides. A flat group provides nothing. There's no hierarchy to walk, so layer numbers don't mean anything. A linear chain provides `hasLayer`, because position in the chain maps cleanly to layer distance. A parent-child group also provides `hasLayer`, because depth in the tree maps to layer distance from the trigger. The picker compares the two sides. A calc shows up in the dropdown only when everything it requires is in the set of things the bound group provides. In practice that comparison produces three states. With a flat group bound, only Fixed appears, because a flat group provides nothing for the cascades to require. Once you bind a linear chain or a parent-child group, both of which provide `hasLayer`, Fixed, Upline Cascade, and Downline Cascade all appear together. Before you've bound any group at all, every calc appears, since Siren doesn't pre-filter your options until you've made a structural choice. That last case is intentional. If the dropdown hid options before you'd even bound a group, you'd have to guess which calc you want by inferring which group to pick, which is the wrong order. ## Check whether yours is currently paying no one Hiding the option in the dropdown does not change a calculation you already saved. So if you picked Upline Cascade or Downline Cascade earlier and the group bound to this Program or Distributor is now flat, your saved cascade is still in place and it pays no one. It does not quietly fall back to Fixed. It pays out zero on every trigger, and your collaborators earn nothing until you fix it. You are likely in this state if the dropdown shows only Fixed, you remember choosing a cascade, and recent activity for this Program or Distributor shows no credits. The flat-group case is silent: Siren writes no error or log for it, so the missing credits are the only signal. Re-binding a group with the right structure, below, starts crediting again. For every state that makes a cascade pay no one, and how to confirm which one you are in, see [cascade troubleshooting](/documentation/calculation-strategies/cascade-troubleshooting). ## How to make a cascade calc appear If you want Upline Cascade or Downline Cascade in the dropdown, bind a collaborator group whose structure is `linearChain` or `parentChild`. The picker re-renders the moment you switch the bound group, and the cascade options appear. Pick the one you want, configure the per-layer points, save. If you later swap that group back to a flat one, the cascade options disappear from the dropdown. The dropdown filter is a frontend convenience, though. It does not rewrite a calc you already saved. Siren does not coerce a saved cascade back to Fixed, and the API will persist a cascade calc against a flat-bound Program or Distributor if a client sends one directly. What protects you is what happens when the calculation runs, not the dropdown. A cascade bound to a group that can't provide layers pays no one when it runs. It credits zero collaborators on every trigger and does not fall back to a single Fixed payout. A Distributor bound to a flat group with Upline Cascade selected pays out nothing every time. A plain flat group does this silently, with no error or log, because there is nothing for the cascade to walk. A group whose structure yields steps that can't report a layer (a misconfigured custom structure) pays no one the same way, and in that case Siren does log the misconfiguration so the silent no-op is traceable. For the full list of states that make a cascade pay no one, and how to diagnose them, see [cascade troubleshooting](/documentation/calculation-strategies/cascade-troubleshooting). If you want a cascade and the group you've bound is flat, the fix is the group, not the calc. Either change the existing group's structure (if it doesn't already have members whose ordering would be lost) or bind a different group that already has the right shape. The [structure overview](/documentation/collaborator-group-structures/choosing-a-collaborator-group-structure) walks through which structure fits which scenario, and the [calc strategy overview](/documentation/calculation-strategies/choosing-a-calculation-strategy) covers when a cascade is the right call at all. > **For developers:** capabilities are string ids declared by walker steps and required by calc strategies. The full list of marker interfaces, how to add a new capability, and how custom structure resolvers advertise what they provide is on the [walker capabilities reference](/documentation/extensions/walker-capabilities). ## Working with Recipes Source: https://www.sirenaffiliates.com/documentation/getting-started/working-with-recipes What recipes are, how to install them, how to share them, and how agencies use them as configuration-as-code for client deployments. Recipes are pre-built incentive program configurations you can install in one click. Each recipe creates a complete setup (programs, program groups, distributors, all with sensible defaults) so you don't have to build anything from scratch. You pick a recipe that matches what you're trying to do, adjust a few customizable fields like commission rate, and hand off to your Siren admin to confirm. This page explains how recipes work, the different ways to install them, and how agencies use them as a configuration-as-code primitive for client deployments. ## The one-click install The most common path is the recipe library at [/recipes](/recipes). Browse the list, find one that matches your use case, and click Install. The recipe page will walk you through any customizable fields (usually things like commission rate, cookie window, or program name) and then hand you off to your Siren admin to confirm the installation. Once you confirm, the programs, groups, and distributors are created and ready to use. This is the path the [Quick Start](/documentation/getting-started/quick-start) guide recommends for new users, and it's the path you should default to whenever a library recipe fits your situation. It's faster than building from scratch and the defaults are battle-tested. ## Recipes are JSON under the hood Every recipe is a JSON document that describes what to create: the programs and their engagement triggers, the program groups and their sorters, the distributors and their metric configurations, and the relationships between them. The customizable fields you see on the install screen are declared in the recipe's frontmatter and map to specific paths inside the JSON. This matters because it means recipes are deterministic and shareable. The same recipe JSON applied to two different Siren installs produces the same setup on both. There's no hidden state, no install-specific logic, no "it worked on my install" surprises. If you have the JSON, you can reproduce the configuration anywhere. ## Importing a recipe from JSON If someone shares a recipe JSON with you (a colleague, an agency, a forum post, an AI assistant), you don't have to publish it to the library to install it. Paste the JSON directly into the Siren admin's recipe import field and it installs exactly the same way as a library recipe. Customizable fields are still respected, defaults still apply, and the end result is identical. This is the supported path for custom or private recipes. Anything you'd build in the library, you can also build as standalone JSON and share directly. ## For agencies: version-controlling client setups If you build incentive programs for multiple clients, treat recipes as configuration-as-code. Save the recipe JSON for each client setup in a git repo or a shared drive, one file per client or one per program shape. When you onboard a new client, paste the matching recipe into their Siren install. When a client's setup changes, update the JSON in version control and re-apply. The same recipe JSON works on any Siren install, so staging-to-production promotion is a copy-paste. Build the recipe on a staging install, confirm it works, then paste the same JSON into production. No migration scripts, no database exports, no manual re-entry. This is the closest thing Siren has to infrastructure-as-code for incentive programs, and it's what makes recipes the strongest single tool in the product for anyone managing multiple installs. You can also keep your recipe library internal. You don't have to publish anything to Siren's library to use recipes this way. The JSON paste import gives you a fully private workflow. ## What recipes can't do today Recipes are powerful, but there are a few gaps worth knowing about before you build a workflow around them. There's no built-in way to export a running Siren install as a recipe JSON. If you've built a custom setup by hand and want to capture it as a recipe, you don't have a one-click path. You either need to have built the setup from a recipe in the first place (so you already have the JSON), or you need to describe the setup to Beacon and ask for the matching recipe. This is a known gap, and exporting from a live install is on the roadmap. Siren also only installs recipes from its own library of trusted sources. You can't point Siren at your own hosted recipe URL and have it pull recipes from there. The workaround is the JSON paste import described above, which works for any recipe you can put on your clipboard. ## Beacon as a recipe assistant For migrations and custom setups, [Beacon](/documentation/getting-started/what-is-beacon) is Siren's free AI assistant that knows the recipe format and can generate recipe JSON for you based on a plain-language description. If you're moving from another platform and want a Siren setup that mirrors what you had, describe the old setup to Beacon and ask for the matching recipe. If you want a recipe shape that doesn't exist in the library yet, describe what you want and paste the result into your install. Beacon is also how you fill the export gap. If you have a running setup you want to capture, describe it to Beacon in enough detail and it can produce a recipe that recreates it. This isn't as clean as a real export, but it's the closest thing available today. ## Where to go next Browse the library at [/recipes](/recipes) to see what's available. For the full Beacon workflow, including how it handles migrations and custom recipes, see [What is Beacon?](/documentation/getting-started/what-is-beacon). --- # Recipes ## Affiliate and Lead Gen Combo Source: https://www.sirenaffiliates.com/recipes/affiliate-and-lead-gen-combo Two programs in a single group, one paying percentage commissions on sales and the other paying a flat bounty per lead. The program group ensures only one fires per opportunity based on first-touch attribution. ## What This Recipe Does This recipe creates two programs bundled in a program group: 1. **Affiliate Program** - 15% commission on referred sales, tracked via referral link visits 2. **Lead Generation Program** - $20 flat bounty per qualified form submission, tracked via form events The program group ties them together with an `oldestBindingWins` sorter. This means the first program to establish an engagement with a prospect takes priority. If a visitor submits a form before clicking an affiliate link, the lead bounty fires. If they click an affiliate link first, the sale commission fires instead. Only one program pays out per opportunity. This structure is built for businesses that run both sale and lead conversion funnels through the same partner network. Instead of choosing between affiliate commissions and pay-per-lead, you get both in a single system with clean attribution. ## Who It's For - **Service businesses with e-commerce** that close some customers through consultations (lead gen) and others through direct purchases (affiliate) - **SaaS companies** where some affiliates drive free trial signups and others drive paid conversions - **Marketing teams** that want one partner program covering the full spectrum of conversions without paying twice for the same prospect ## How It Works The Affiliate Program tracks referral link visits. When a collaborator shares their unique link and a visitor clicks through, that engagement is recorded. If the visitor later makes a purchase, the collaborator earns a 15% commission on the transaction. The Lead Generation Program tracks form submissions. When a visitor who was referred by a collaborator submits a connected form (through a compatible plugin like Gravity Forms), that triggers a flat $20 bounty. No sale is required. The program group is what makes this recipe work. Without it, both programs would fire independently, and you could end up paying a lead bounty and a sale commission for the same person. The `oldestBindingWins` sorter resolves this by giving priority to whichever program established the first engagement. If a prospect fills out a form on their first visit, the lead program claims them. If they arrive through a referral link first, the affiliate program claims them. This first-touch model encourages collaborators to find new audiences. The program that reaches a prospect first wins the attribution, so there is no advantage to retargeting people already in the funnel. > Can I run a lead-gen program and an affiliate program on the same site without them fighting over the same sale? ### Program Snapshot - Best for: Businesses converting through both checkout sales and lead capture forms - Main goal: One partner network paid on leads and sales without double payouts - Partners involved: Collaborators who drive purchases, form fills, or both - Actions tracked: Referred site visits and connected form submissions - Rewards supported: Percentage sale commissions and flat per-lead bounties - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Affiliate Program: 15% percentage of transaction, newest engagement wins attribution, tracked via Referral links - Lead Generation Program: $20.00 fixed per lead, oldest engagement wins attribution, tracked via Form submissions - Program group Affiliate and Lead Gen: oldest engagement wins across affiliate, leadGen ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is an affiliate and lead generation program?** It's a partner program that rewards one network for captured leads as well as completed sales. In this recipe, a qualified form submission pays a flat $20 bounty and a referred sale pays a 15% commission. Because the two programs share a group, each prospect is credited to exactly one of them, decided by whichever made first contact. **How does an affiliate and lead generation program work?** Partners share referral links and lead forms, and Siren credits each prospect to whichever touch arrived first. A form submitted before any link click pays the $20 bounty, while a purchase that began with a link click pays the 15% commission. That first-touch rule pushes partners toward audiences you haven't reached yet rather than prospects already in your funnel. **Why does the program group use oldestBindingWins?** First-touch attribution determines which program fires. If a lead submits a form before clicking an affiliate link, the lead bounty pays out. If someone clicks an affiliate link first, the sale commission takes priority. The first engagement to establish a binding wins. **Can a collaborator be enrolled in both programs at once?** Yes. A collaborator can participate in both the affiliate and lead gen programs. The program group prevents both from firing on the same opportunity, but a collaborator can earn sale commissions for some prospects and lead bounties for others. **What happens if I remove the program group?** Both programs will operate independently. A single prospect could trigger both a lead bounty and a sale commission, which means you would pay twice for the same opportunity. Keep the group unless you intentionally want stacking. **What Siren plan do I need for this recipe?** Essentials. Program groups are an Essentials feature. Without the group, the two programs would stack rather than compete, which changes the economics significantly. ## Affiliate and Royalty Stack Source: https://www.sirenaffiliates.com/recipes/affiliate-and-royalty-stack Two independent programs that fire on the same transaction. Affiliates earn 25% for driving the sale, and product creators earn 50% royalty when their content sells. Both pay out simultaneously because the programs are intentionally ungrouped. ## What This Recipe Does This recipe creates two independent programs designed to pay different people from the same transaction: 1. **Affiliate Program** - 25% commission to the person who referred the buyer, tracked via referral link visits 2. **Creator Royalty Program** - 50% royalty to the person who created the product, tracked via product ownership bindings These programs are deliberately not grouped. When a customer buys a product, both programs evaluate the transaction independently. The affiliate who drove the traffic earns 25%, and the creator who built the product earns 50%. Two different people, two different roles, both compensated from one sale. This is the foundational model for course platforms, digital marketplaces, and any creator economy where you need to reward both the builder and the promoter. ## Who It's For - **Course platform operators** building a marketplace where instructors create courses and affiliates drive enrollments - **Digital product marketplace owners** who need to compensate both product creators and the affiliates who sell their work - **Creator economy platforms** where content producers and promoters are different people with different compensation models ## How It Works The Affiliate Program uses the `referredSiteVisit` engagement type. When someone clicks a collaborator's referral link and later makes a purchase, the affiliate earns 25% of the transaction. Attribution uses `newestBindingWins`, so the most recent referral link click determines who gets credit. This is standard affiliate behavior. The Creator Royalty Program uses the `collaboratorProductSold` engagement type. This is fundamentally different from referral tracking. Instead of tracking who sent the buyer, it tracks who created the product. When a creator is bound to a product in Siren and that product sells, the creator earns 50% of the line item total. No referral link is needed because the engagement is tied to product ownership, not traffic. The two programs coexist because they answer different questions. The affiliate program asks "who sent the buyer?" while the royalty program asks "who made the product?" Since these are always different people (or at least different roles), stacking them is the correct behavior. A platform owner running a course marketplace wants both the instructor and the affiliate to earn from every sale. If you want to cap total payouts per transaction, you can adjust the percentages. A 25% affiliate rate plus a 50% royalty rate means 75% of each sale goes to collaborators, leaving 25% as platform revenue. Adjust these rates to match your margin targets. > Can I pay both the affiliate who drove the sale AND the creator who built the product, on the same order? ### Program Snapshot - Best for: Course platforms and digital marketplaces paying creators and promoters - Main goal: Pay both the referring affiliate and the product creator per sale - Partners involved: Affiliates who refer buyers and creators who own the products - Actions tracked: Referral link visits and creator-bound product sales - Rewards supported: 25% affiliate commission stacked with a 50% creator royalty - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Affiliate Program: 25% percentage of transaction, newest engagement wins attribution, tracked via Referral links - Creator Royalty Program: 50% percentage of transaction, newest engagement wins attribution, tracked via Owned product sales ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is an affiliate and royalty program?** It's two payout structures running on the same store at once. The affiliate side pays a commission to whoever referred the buyer, and the royalty side pays the creator who owns the product that sold. On a marketplace or course platform, that means one order can compensate two different people for two different contributions. **Who should use an affiliate and royalty program?** Platforms where the person who makes the product isn't the person who promotes it. Course marketplaces are the clearest case, with instructors building courses while affiliates drive enrollments, and the same split shows up anywhere building and selling are separate jobs. If you only have one of those roles, a single affiliate or royalty program is simpler. **How can both programs fire on the same sale?** Because the programs are not in a program group. Siren's default behavior is to let every active program evaluate every transaction independently. Without a group enforcing mutual exclusivity, both the affiliate commission and the creator royalty calculate and pay out on the same sale. **Does the affiliate earn on their own purchases?** Only if they referred themselves, which is uncommon. The affiliate program tracks referral link visits, so a sale needs a prior referral event to trigger a commission. Self-referral policies are configurable in Siren's settings. **What if a product has no creator bound to it?** The royalty program only fires when the product sold has a collaborator bound to it via the collaboratorProductSold engagement type. If no creator is bound, the royalty program simply does not activate for that transaction. The affiliate commission still pays out normally. **Can I add more programs to this stack?** Yes. You can add any number of independent programs. For example, you could add a platform fee distributor or a customer referral bonus. As long as programs are not grouped together, they all evaluate independently. ## Ambassador and Affiliate Dual Program Source: https://www.sirenaffiliates.com/recipes/ambassador-and-affiliate-dual-program Two independent programs running side by side. A high-commission ambassador program for hand-picked partners and a standard affiliate program open to anyone, each with its own commission rate. ## What This Recipe Does This recipe creates two independent programs: 1. **Ambassador Program** - 30% commission for hand-picked brand ambassadors, tracked via both referral links and coupon codes 2. **Affiliate Program** - 15% commission for the general affiliate program, tracked via referral links These programs are not grouped together. Both can fire on the same transaction if a customer was referred by an affiliate and also uses an ambassador's coupon code. This is intentional: it lets you reward both the affiliates driving traffic and the ambassadors providing credibility. Choose this recipe when you have two distinct partner relationships and you want both to coexist without competing for the same commission. ## Who It's For - **Brands with strategic partnerships** where certain partners deserve a premium commission rate and dedicated support - **Businesses running both invite-only and open programs** who want clear separation between their ambassador and affiliate tracks - **Companies with influencer relationships** where ambassadors promote via coupon codes while general affiliates use referral links ## How It Works The Ambassador Program is your invite-only, premium track. You hand-pick your ambassadors, give them personalized coupon codes, and they earn 30% on any sale that involves their code or referral link. Both engagement types are enabled so ambassadors have maximum flexibility in how they promote. Some prefer sharing links, others prefer coupon codes, and many use both. The Affiliate Program is your open-enrollment track. Anyone can sign up, grab a referral link, and earn 15% on the sales they refer. This program only tracks referral link visits, keeping the barrier to entry low and the mechanics simple. Because these two programs are not grouped, they operate completely independently. A single sale can trigger commissions in both programs at the same time. For example, if a customer clicks an affiliate's referral link and also enters an ambassador's coupon code at checkout, both the affiliate and the ambassador earn their respective commissions. This stacking behavior is deliberate: it rewards every partner who contributed to the sale. If you later decide you do not want commissions to stack, you can add a program group to make the two programs mutually exclusive. > Can I run a high-commission ambassador tier and an open affiliate program at the same time? ### Program Snapshot - Best for: Brands running invite-only ambassadors next to open affiliates - Main goal: Pay strategic partners a premium rate on a separate track - Partners involved: Hand-picked brand ambassadors plus open-enrollment affiliates - Actions tracked: Referral link visits and coupon code use at checkout - Rewards supported: Percentage-of-sale commissions, one rate per track - Starting point: Start free with Siren Lite ### What This Recipe Configures - Ambassador Program: 30% percentage of transaction, newest engagement wins attribution, tracked via Referral links, Coupon codes - Affiliate Program: 15% percentage of transaction, newest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a brand ambassador program?** A brand ambassador program is an invite-only arrangement where a brand recruits specific people to represent it and pays them for the sales they generate, usually at a better rate than a general affiliate earns. In this recipe, ambassadors earn 30% by default and can promote through a referral link, a personalized coupon code, or both. **How does a brand ambassador program work?** You select the ambassadors yourself instead of opening enrollment, give each one a tracked link and a coupon code, then pay a set percentage on the orders they drive. Running it as its own program means the premium rate never bleeds into your standard affiliate track. Each track holds onto its own rate and roster, so adjusting one doesn't touch the other. **Can a person be enrolled in both programs at once?** Technically yes, but it usually makes more sense to keep them in one track. An ambassador enrolled in both would earn commissions from each program on the same sale. **Why aren't the programs in a group?** By design. The ambassador and affiliate tracks serve different purposes and often involve different people. Keeping them independent lets a sale reward both the affiliate who drove the traffic and the ambassador whose coupon closed the deal. **Can I make the ambassador program track only coupon codes?** Yes. After applying the recipe, remove the referredSiteVisit engagement type from the Ambassador Program so ambassadors only earn when their coupon codes are used. **What Siren plan do I need?** This recipe works on Lite. Both programs are standalone, so no program groups or advanced features are required. ## B2B Referral Program Source: https://www.sirenaffiliates.com/recipes/b2b-referral-program B2B referral program software for high-value partnerships. Pays a flat $50 bounty per referred sale with first-touch attribution, built for businesses where referrals are infrequent but each conversion carries significant value. ## What This Recipe Does This recipe creates a B2B referral program that pays a flat $50 for every referred sale. Referrers share a unique link to your site. When a prospect clicks that link and eventually makes a purchase, the referrer earns the reward. The payout is the same whether the deal is worth $200 or $20,000. Fixed rewards work better than percentages in B2B for two reasons. First, B2B deal sizes vary enormously, and a percentage-based reward could range from trivial to enormous on a single referral. A flat amount keeps your costs predictable. Second, referrers in B2B are usually existing clients or professional contacts, not marketing affiliates. A clear, simple reward is easier to communicate and more motivating than a complex percentage calculation. ## Who It's For - **B2B companies** that want to formalize word-of-mouth referrals from satisfied clients into a trackable, rewarded program - **Service businesses** (agencies, consultancies, SaaS) where a single new client can be worth thousands in lifetime revenue and a $50 referral reward is a small acquisition cost - **Professional networks** where partners, vendors, and clients make introductions that lead to new business ## How It Works When you apply this recipe, Siren creates a program that tracks referred site visits and uses first-touch attribution. Each referrer gets a unique link. When a prospect clicks that link, Siren records the referral and binds the prospect to the referrer. First-touch attribution is critical for B2B. Sales cycles can run weeks or months. A prospect might click a referral link in January, visit your site several times on their own, attend a demo in February, and finally purchase in March. With first-touch attribution, the person who made the original introduction keeps credit through the entire cycle. Later interactions do not override the original referral. The $50 reward is stored as 5000 cents in the recipe JSON. This flat amount fires once per qualifying transaction, regardless of the order value. For most B2B businesses, $50 per referred sale is a reasonable starting point. If your average deal value is higher, increase the reward to make the program more attractive to referrers. If you sell lower-ticket B2B products, you might reduce it. Commissions are calculated on line items only. Shipping, taxes, and fees are excluded, though for a fixed-amount reward the transaction total does not affect the payout. > How do I run a simple B2B referral program that pays a flat bounty per deal? ### Program Snapshot - Best for: Service businesses and B2B companies formalizing client referrals - Main goal: Turn introductions from clients and partners into tracked, rewarded deals - Partners involved: Existing clients, business partners, professional contacts - Actions tracked: Referral link clicks and completed purchases, credited first-touch - Rewards supported: Flat bounty per referred sale - Starting point: Start free with Siren Lite ### What This Recipe Configures - B2B Referral Program: $50.00 fixed per transaction, oldest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Fixed-Rate Affiliate Program (https://www.sirenaffiliates.com/recipes/fixed-rate-affiliate-program): The Fixed-Rate Affiliate Program runs the same flat-fee-per-sale machinery but credits whichever referral link the buyer clicked most recently, and its default commission is $10 instead of the $50 bounty the B2B Referral Program starts with. Pick the Fixed-Rate Affiliate Program when you recruit marketing affiliates driving short purchase cycles where the last link clicked should earn the payout. - Refer-a-Friend Program (https://www.sirenaffiliates.com/recipes/refer-a-friend-program): The Refer-a-Friend Program hands referral links to your existing customers and pays a default $10 per friend purchase, with the newest link winning credit if a friend arrives through two different customers' links. The B2B Referral Program protects the first introducer instead. Switch to the Refer-a-Friend Program when everyday buyers, not business contacts, are doing the sharing and purchases close quickly. ### Frequently Asked Questions **What is a B2B referral program?** A B2B referral program rewards the people who introduce new business to you: clients, partners, and professional contacts. Instead of informal thank-yous, every introduction is tracked through a referral link and pays a defined reward when the deal closes. **Who should use a B2B referral program?** Agencies, consultancies, SaaS companies, and any business where new clients arrive through relationships. If a single client is worth thousands in lifetime revenue, a structured referral reward is one of the cheapest acquisition channels available. **Why a flat reward instead of a percentage?** B2B deal sizes vary widely. A flat reward gives you predictable referral costs regardless of whether the referred deal is worth $500 or $50,000. It also makes the program easy to communicate to referrers. **What if a prospect clicks one referral link and then another before buying?** The first referrer keeps credit. This program uses first-touch attribution because B2B sales cycles are long. The person who made the original introduction deserves the reward, even if the prospect encounters other referral links during their research. **Can I increase the reward amount above $50?** Yes. Adjust the incentive amount when you apply the recipe or change it later in the Siren admin. The amount is stored in cents internally, so $100 would be 10000. **Do referrers need special tools or dashboards?** Referrers share a unique link to your site. When someone clicks that link and eventually makes a purchase, Siren handles the tracking and attribution automatically. No special tools are required. ## Basic Affiliate Program Source: https://www.sirenaffiliates.com/recipes/basic-affiliate-program A standard WordPress affiliate program for WooCommerce. Affiliates share referral links and earn a percentage commission on every sale. The simplest way to launch affiliate tracking with Siren. ## What This Recipe Does This recipe creates a single affiliate program with percentage-based commissions. Affiliates share unique referral links to your store. When a visitor clicks an affiliate's link and makes a purchase, the affiliate earns 20% of the transaction total. This is the most common affiliate program structure and the recommended starting point for most stores. If you are new to affiliate marketing or just want a straightforward, proven setup without tiers or special partner tracks, this is the right recipe. ## Who It's For - **E-commerce store owners** who want to drive more sales through word-of-mouth referrals - **Digital product sellers** looking to incentivize bloggers, influencers, or existing customers to promote their products - **Anyone new to Siren** who wants a simple, proven program structure to learn the system ## How It Works When you apply this recipe, Siren creates a single program that watches for referred site visits. Every time someone lands on your store through an affiliate's unique link, Siren notes who sent them. If that visitor goes on to buy something, the affiliate who referred them earns a commission of 20% of the order's line item total. If a customer clicks links from multiple affiliates before purchasing, the most recent referral gets credit. This "newest engagement wins" approach keeps things simple and fair: the affiliate whose link was freshest in the customer's journey is the one who gets paid. Commissions are calculated on line items only, so shipping, taxes, and fees are excluded. This gives you a clean, predictable cost structure for your affiliate program. ## Setting Up Recurring Commissions on Subscriptions If you're running a membership site or selling subscriptions, you can extend this recipe to pay affiliates on every renewal, not just the first sale. Siren handles this through a dual-program pattern: install this Basic Affiliate Program as your first-sale reward, then create a second program manually that fires only on renewal conversions at whatever recurring rate you want to pay. Both programs target the same engagement triggers, so the affiliate who referred the customer earns on the initial purchase and on each recurring payment afterward. The two programs stack on the first sale, which lets you pay a higher rate up front and a smaller rate on renewals. For example, the initial sale might pay 25% total (this recipe's 20% plus 5% from the renewal program), and each renewal pays just the 5% from the renewal program. To build the second program, create a new program with the same engagement tracking events and program structure as this one, set the incentive to your preferred recurring rate, and check only the "renewals" conversion box so it doesn't fire on the initial sale. The affiliate stays attributed through Siren's binding system, so renewals that happen months later still credit the original referrer. For a longer walkthrough of subscription affiliate programs and the configuration details, see [Subscription Programs](/documentation/getting-started/subscription-programs). > How do I launch a basic affiliate program on my WooCommerce store? ### Program Snapshot - Best for: Stores and digital product sellers launching their first affiliate program - Main goal: Drive referred sales through partner-shared links - Partners involved: Affiliates, bloggers, influencers, existing customers - Actions tracked: Referral link clicks and completed purchases - Rewards supported: Percentage commission on every referred sale - Starting point: Start free with Siren Lite ### What This Recipe Configures - Affiliate Program: 20% percentage of transaction, newest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Course Affiliate Program (https://www.sirenaffiliates.com/recipes/course-affiliate-program): The Course Affiliate Program shares the Basic Affiliate Program's mechanics, with referral links, last-click credit, and commissions calculated on line item totals, but it defaults to a 30% rate instead of 20% and is built for LifterLMS and LearnDash catalogs sold through WooCommerce. Pick the Course Affiliate Program when you sell online courses through an LMS and want a default rate that matches digital product margins. - First-Touch Referral Program (https://www.sirenaffiliates.com/recipes/first-touch-referral-program): The First-Touch Referral Program flips the Basic Affiliate Program's attribution rule. Where the Basic Affiliate Program credits the newest referral before checkout, the first-touch version binds each visitor to the original referrer, so the affiliate who introduced the buyer keeps the 20% commission even when a later click comes from someone else. Choose the First-Touch Referral Program when buyers research for days or weeks before purchasing and the introduction deserves the payout more than the final click. ### Frequently Asked Questions **What is an affiliate program?** An affiliate program rewards partners for sending you customers. Each affiliate shares a unique link, and when someone clicks it and buys, the affiliate earns a commission on the sale. It turns word-of-mouth into a measurable, paid channel. **Who should use an affiliate program?** Any store or digital product business that wants partners promoting it. If customers, bloggers, or creators already recommend your products, an affiliate program gives them a reason to do it more and gives you the data to reward it fairly. **Can I change the commission rate after installing?** Yes. Edit the program's incentive amount in your Siren admin to adjust the rate at any time. **What happens if a customer clicks two different affiliate links?** The most recent referral wins. Siren credits the affiliate whose link the customer clicked last. **Do I need WooCommerce for this to work?** Yes. Siren calculates commissions from WooCommerce transactions, so WooCommerce must be installed and active. **Can I run this alongside other programs?** Absolutely. Siren supports multiple programs at once. This affiliate program will operate independently unless you group it with another program. ## Blog Content Program Source: https://www.sirenaffiliates.com/recipes/blog-content-program A commission program that attributes sales to collaborators through content views instead of referral clicks. Pay writers, guest experts, interview subjects, and content partners when a buyer reads a post bound to them before purchasing, no affiliate link required. ## What This Recipe Does This recipe creates a single program that pays a collaborator a commission when one of their bound blog posts influenced a sale. Attribution fires when the customer reads the post, not when they click a link, so the collaborator doesn't need to distribute a tracking URL at all. The content itself is the tracking primitive. That opens up a wider range of partnership structures than a link-based affiliate program supports. A guest expert you interview for a recap post earns on sales attributable to that interview without having to share a link. A creator you feature in a webinar recap earns when viewers read the post afterward. A writer contributing a long-form article on your site earns royalty on purchases from readers of that article, even if the reader found the article through search, email, social, or an AI recommendation with no referral click in the chain. The program is also an insurance layer for AI-mediated commerce. Customers arriving from ChatGPT or similar tools frequently skip the affiliate click entirely but still land on and read the content before buying. The content view fires the attribution event whether a click ever happens. ## Who It's For - **Editorial sites and multi-author publications** where writers expect credit for posts that moved readers toward a purchase - **Cross-promotional programs** where guest experts, interview subjects, and webinar partners earn on sales driven by their recap content - **Content-driven e-commerce stores** that want attribution to fire even when readers arrive from AI tools and bypass normal affiliate links - **Tight partnership tiers** where a small group of collaborators each own a dedicated content piece and want attribution tied directly to that artifact - **Video and podcast partnerships** where the creator's content is embedded in a post on your site and the post itself carries the attribution binding ## How It Works When you apply this recipe, Siren creates a single program that listens for the `boundPostUsed` event. This event fires every time a reader lands on a blog post bound to a collaborator. When the reader later completes a purchase, Siren awards the commission based on the program's resolver. The binding between collaborator and post happens through WordPress's native post author, so most setups need no manual configuration beyond adding each collaborator in Siren. Posts the collaborator publishes from that point forward are automatically attributable. You can also bind existing posts to a collaborator manually, which is useful for guest-authored pieces published under an editor's byline or for transferring attribution when content changes hands. Compared to a standard affiliate program, this setup removes the need for a click entirely. The collaborator doesn't have to distribute a tracking link, the reader doesn't have to click anything special, and agentic commerce flows that skip browser navigation still produce the engagement event when the customer reads the content on your site. Attribution binds to the artifact (the post view), not to a click that may never happen. Commissions are calculated on line items with discounts subtracted and fees included, so the commission base reflects the actual revenue the order produced. Shipping and taxes are excluded. ## Choosing the Right Attribution Model The default for this recipe is newest-binding-wins, which matches how most affiliate programs treat multiple referral clicks: whichever collaborator's content was read most recently before the sale wins the full commission. This is the simplest model and the right starting point for most programs. You can swap the resolver to suit your partnership style. First-touch rewards the collaborator who introduced the customer to your content, even if other collaborators' posts were read later. An evenly-shared pool credits every collaborator whose content contributed to the journey, regardless of order or frequency. A performance-weighted split scores collaborators by how many of their posts were read and divides the commission proportionally, so a contributor with three read posts earns more than a contributor with one. Different partner tiers often deserve different models. You can run more than one of these programs side by side and scope them to different content categories or partner types so that newsroom writers, guest experts, and webinar partners each get paid in the way that fits their contribution. > How do I pay collaborators a commission when a buyer reads their published content on my site before checkout, without requiring an affiliate link click? ### Program Snapshot - Best for: Editorial sites and stores paying guest experts and content partners - Main goal: Attribute sales to the content that influenced them - Partners involved: Writers, guest experts, interview subjects, webinar partners - Actions tracked: Bound post reads and completed purchases - Rewards supported: Percentage commission on sales influenced by bound content - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Blog Content Program: 15% percentage of transaction, newest engagement wins attribution, tracked via Blog post visits ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a content partnership program?** A content partnership program pays collaborators when content they contributed influences a sale. Instead of handing partners referral links, you bind published posts to the people behind them, and a commission fires when a buyer reads that content before checking out. It's commission infrastructure for guest experts, interview subjects, and contributing writers. **Who should use a content partnership program?** It fits sites where published content does the selling, like editorial stores, multi-author publications, and brands that run interview or webinar series with outside experts. If you want a guest's post to keep earning for them long after publication, this structure works better than a link-based affiliate program, because the post itself carries the attribution. **How is this different from a standard affiliate program?** A standard affiliate program fires on a referral link click. This program fires when a reader views a blog post bound to one of your collaborators. No link, no cookie, no UTM parameter needed. If the customer reaches checkout after reading the content, the collaborator who owns that content earns the commission. **What happens if a customer reads posts from two different collaborators before buying?** That depends on how you configure the program. Siren supports several attribution models, and one of the reasons to run content attribution on Siren rather than a rigid SaaS platform is that you can pick the one that fits how you want to pay. The default this recipe ships with is newest-binding-wins, so the collaborator whose post was read most recently takes the commission. You can swap that for first-touch (original introducer wins), an evenly-shared pool across every contributor, or a performance-weighted split that scores collaborators by how many of their posts the buyer read. Different partner types often deserve different models, and Siren lets you run more than one program at once so you don't have to pick one global answer. **How does Siren know which content belongs to which collaborator?** Each collaborator is added in Siren and bound to the WordPress posts they authored. For most setups, Siren picks up authorship automatically through the WordPress post author, so posts a collaborator publishes from that point forward are attributable without manual mapping. You can also manually bind existing posts to a collaborator when the published author isn't the right person to credit (for example, a ghost-written interview recap or a guest-contributed article published under the editor's byline). **Does this work when the customer arrives from ChatGPT or another AI tool?** Yes, and that's one of the main reasons to run this setup. Even when an AI-mediated referral skips the click entirely, the customer still reads your content on your site before buying. The boundPostUsed event fires on that read, attribution sticks, and the collaborator earns commission on the sale. No click has to happen anywhere in the chain. **Can I use this with pages, courses, or custom post types instead of blog posts?** The boundPostUsed trigger fires on WordPress blog posts (post_type=post) out of the box. Courses and lessons have their own engagement triggers (lessonCompleted and courseCompleted in Essentials) that you can combine with this program pattern if you want course content to drive attribution. For landing pages or custom post types, talk to the Siren team about extending the trigger, or publish the content as a blog post for the standard setup to cover it. **What Siren tier do I need?** This recipe requires the Essentials tier. The boundPostUsed engagement trigger that powers blog post tracking is an Essentials feature. ## Blogger Revenue Program Source: https://www.sirenaffiliates.com/recipes/blogger-revenue-program A performance-weighted revenue share for bloggers and content creators on your WordPress site. Writers earn commissions proportional to the traffic their posts generate, with higher-traffic authors receiving a larger share of each sale. ## What This Recipe Does This recipe creates a single program that pays bloggers and content creators based on how much traffic their posts contribute to your store's sales. Instead of splitting commissions equally or awarding everything to one person, Siren weighs each blogger's engagement score and divides the commission proportionally. A blogger whose post drives 70% of the engagement leading to a sale earns 70% of that sale's commission. A blogger who contributed 10% earns 10%. This keeps payouts fair and directly tied to performance, rewarding the writers who actually move the needle. Choose this recipe when you run a multi-author WordPress site and want to turn your content team into a revenue-generating force with transparent, traffic-based compensation. ## Who It's For - **Multi-author blog owners** who want to reward writers proportionally based on the traffic their posts generate - **Content-driven WooCommerce stores** where blog posts are a primary sales channel and authors should share in the revenue they create - **Publishers and media sites** building a performance-based creator program that pays for results, not just output ## How It Works When you apply this recipe, Siren creates a program that tracks two types of engagement: blog post visits and referred site visits. Blog post visits fire automatically when a reader lands on a post written by one of your collaborators. Referred site visits fire when someone clicks a blogger's unique affiliate link. Both engagement types contribute to the blogger's score. The key difference from a standard affiliate program is the resolver: this recipe uses a performance-weighted pool instead of "newest engagement wins." When a customer makes a purchase, Siren looks at all the engagement events that led to the sale and divides the commission among every collaborator who contributed. The split is proportional to each person's engagement score. For example, say a customer reads three blog posts before buying. Two were written by Alice and one by Bob. Alice also referred the customer through her affiliate link. Alice's total engagement score is higher, so she receives a larger share of the 15% commission. Bob still earns his portion for the post that contributed to the journey. Nobody gets shut out. This model works especially well for content teams where multiple writers contribute to the customer journey over time. It eliminates the "winner takes all" problem and gives every contributor a fair cut. > What's the best way to share revenue with bloggers based on the traffic their posts actually bring in? ### Program Snapshot - Best for: Multi-author blogs and content-driven WooCommerce stores - Main goal: Turn the blog itself into ongoing revenue for its writers - Partners involved: Bloggers, authors, and content creators on your site - Actions tracked: Blog post visits and affiliate link clicks, both scored per writer - Rewards supported: Percentage commission split through a performance-weighted pool - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Blogger Revenue Share: 15% percentage of transaction, performance weighted attribution, tracked via Blog post visits, Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a blogger revenue share program?** A blogger revenue share program pays writers a cut of the sales their content helps create. Instead of a flat fee per article, each author earns an ongoing percentage of revenue, weighted by how much traffic their posts contribute to each purchase. It turns the blog itself into a recurring income source for the people writing it. **Who should use a blogger revenue share program?** It fits multi-author blogs, publishers, and WooCommerce stores where content does the selling. If your writers' posts pull readers toward products, a revenue share gives them a direct stake in the results. It's especially useful when several authors touch the same buyer's journey, since the pool splits credit proportionally instead of picking one winner. **How does Siren know which blogger deserves credit for a sale?** Siren tracks two engagement types: blog post visits (boundPostUsed) and affiliate link clicks (referredSiteVisit). Each engagement scores the blogger who triggered it. When a sale happens, commissions are split proportionally based on those scores. **What if only one blogger has any engagement on a transaction?** That blogger receives the entire commission. The pool math still applies, but with only one contributor the full share goes to them. **Can bloggers also share affiliate links?** Yes. This recipe tracks both blog post visits and referred site visits. A blogger who writes popular posts and shares referral links earns engagement credit from both activities. **What Siren plan do I need?** This recipe requires the Essentials tier. The performance-weighted pool resolver is an Essentials feature. ## Business Partner Revenue Share Source: https://www.sirenaffiliates.com/recipes/business-partner-revenue-share A revenue sharing program for formal business partnerships. The partner earns an ongoing percentage of sales from the customers assigned to them, with no tracking links or coupon codes required. ## What This Recipe Does This recipe creates a revenue sharing program for business partnerships. Partners earn 5% of every order placed by the customers assigned to them. You bind a customer to a partner once, and from then on every order that customer places credits the partner automatically, with no tracking links or coupon codes. This makes it the right fit for formal business relationships where a partner owns an ongoing book of customers. Unlike a typical affiliate program where customers click links and the system figures out who to pay, this program credits a partner based on who the customer belongs to. You assign each partner's customers to them in the admin, or enable auto-bind on first conversion, making it ideal for co-founder splits, wholesale relationships, joint ventures, and strategic alliances. ## Who It's For - **Business owners with revenue-sharing agreements** who need a structured way to track and pay partner commissions on attributed sales - **Wholesale and distribution partners** who bring in customers through relationships rather than digital marketing - **Strategic alliance managers** who attribute sales through internal records, contracts, or conversations rather than tracking pixels ## How It Works When you apply this recipe, Siren creates a program that credits the partner whenever a customer bound to them makes a purchase. Each business partner is added as a collaborator. You bind customers to a partner in the Siren admin, or enable auto-bind on first conversion, and because the binding persists, the partner keeps earning on those customers' future orders automatically. The "newest binding wins" resolver applies when more than one binding could claim the same customer. If a customer is bound to partner A and later rebound to partner B, the most recent binding takes priority. In practice this rarely creates conflicts because each customer belongs to one partner at a time. The 5% default rate reflects a common structure for passive revenue sharing, where the partner's contribution is introductions or brand alignment rather than active selling. You can adjust this to any percentage that fits your partnership agreement. Some revenue shares run at 50/50 for co-founders, while others sit at 2-3% for referral partnerships. Set the rate that matches your deal. Commissions are calculated on line item totals only. Shipping, taxes, and fees are excluded from the calculation. > Can I set up a revenue share with a business partner without tracking links or coupons? ### Program Snapshot - Best for: Businesses with formal revenue-sharing agreements and strategic partners - Main goal: Pay partners an ongoing percentage of the revenue they bring in - Partners involved: Co-founders, investors, wholesalers, distributors, strategic allies - Actions tracked: Every order from customers assigned to the partner, credited automatically - Rewards supported: Percentage of each attributed sale, calculated on line items - Starting point: Start free with Siren Lite ### What This Recipe Configures - Partner Revenue Share: 5% percentage of transaction, newest engagement wins attribution, tracked via opportunityBoundToCollaborator ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Frequently Asked Questions **What is a partner revenue share program?** A partner revenue share program pays a business partner an ongoing percentage of the sales their relationship produces. It's the structured version of a handshake deal: rather than a one-off referral bounty, the partner earns a defined cut of every transaction you attribute to them, for as long as the agreement runs. **Who should use a partner revenue share program?** Anyone whose revenue-sharing agreement lives in a contract rather than a tracking pixel. That includes co-founders splitting income, investors with a revenue stake, wholesale and distribution partners, and strategic alliances where you already know which sales came from which relationship. **How do I attribute a sale to a partner?** You assign the partner's customers to them once in the Siren admin, or let Siren auto-bind a customer on their first purchase. Siren then credits the partner on every future order those customers place. **Can I use this for ongoing revenue splits, not just one-time referrals?** Yes. The customer-to-partner binding is persistent, so every future order from a bound customer credits the partner automatically. This is true lifetime revenue sharing, not a one-off. **What if I want different partners to earn different percentages?** This recipe sets a single rate for all partners in the program. For per-partner rates, create separate programs for each partner or adjust the settings after applying the recipe. **Why is there no tracking link or coupon code?** Attribution is by who the customer belongs to, the partner relationship, not by customer behavior. Because each customer is bound to a partner, Siren already knows who to credit, so no link or coupon is needed. **Does this require a specific Siren plan?** Yes. Crediting a partner for every order from customers bound to them, ongoing and with no link or coupon, is lifetime attribution, which is a Siren Plus feature. ## Channel Partner Program Source: https://www.sirenaffiliates.com/recipes/channel-partner-program A channel partner management and commission tracking program for resellers, distributors, and B2B partners. Tracks sales through partner-specific coupon codes and through the accounts you assign to each partner, with first-touch credit that rewards long-term relationships. ## What This Recipe Does This recipe creates a channel partner commission program built for resellers, distributors, and B2B partners. Partners earn 15% of every sale attributed to them, tracked through two methods: partner-specific coupon codes and the accounts you assign to each partner. The program uses first-touch attribution, so the partner who originally established the customer relationship keeps credit for future sales. This is not a typical affiliate program. Channel partnerships are built on long-term business relationships, not one-time link clicks. The dual tracking approach handles both digital transactions (coupon codes at checkout) and owned accounts (you assign a partner's customers to them, and every order those customers place credits the partner automatically, covering phone sales, trade show contacts, and direct relationships). ## Who It's For - **Businesses with reseller or distributor networks** that need to track and pay commissions on partner-referred sales automatically - **B2B companies** whose channel partners use branded discount codes when referring clients to purchase online - **Sales teams managing hybrid attribution** where some deals come through coupon codes at checkout and others are closed through direct conversations or trade events ## How It Works When you apply this recipe, Siren creates a program that tracks two engagement types: bound coupon usage and customers bound to a collaborator. Each channel partner is added as a collaborator and can be assigned a WooCommerce coupon code tied to their profile. When a customer uses that code at checkout, the partner gets credit. For relationships that close outside your website, you assign the partner's accounts or customers to them, and because the binding persists, every future order those customers place credits the partner automatically alongside the coupon path. The "oldest binding wins" resolver is what sets this apart from standard affiliate programs. In channel sales, the partner who brought the customer to the table first should keep that relationship. If Partner A introduces a client through their coupon code in January, and Partner B somehow interacts with the same client in March, Partner A retains credit. This protects long-term partner investments and prevents territory disputes. The 15% default rate sits in the middle of typical channel commission ranges. Adjust it based on your margins and what your partners expect. Software resellers often earn 15-30%, while product distributors may earn 5-15%. Set the rate that makes your channel economics work. Commissions are calculated on line item totals only. Shipping, taxes, and fees are excluded from the calculation. > How do I build a channel partner or reseller program with coupon-based tracking and first-touch credit? ### Program Snapshot - Best for: Companies selling through reseller and distributor networks - Main goal: Pay channel partners automatically on the sales they close - Partners involved: Resellers, distributors, and B2B channel partners - Actions tracked: Partner coupon use at checkout, plus every order from the accounts assigned to a partner - Rewards supported: 15% commission on attributed sale line items - Starting point: Start free with Siren Lite ### What This Recipe Configures - Channel Partner Program: 15% percentage of transaction, oldest engagement wins attribution, tracked via Coupon codes, opportunityBoundToCollaborator ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Frequently Asked Questions **What is a channel partner program?** A channel partner program pays outside companies, usually resellers and distributors, a commission for selling your products to their own customers. It differs from an affiliate program built on link clicks: channel deals close through business relationships, so sales get tracked through partner coupon codes and direct attribution instead of web traffic. **Who should use a channel partner program?** Businesses that sell through other businesses. That means software vendors with reseller networks, manufacturers with distributors, and B2B companies whose partners recommend or resell their products to clients. If a meaningful share of your revenue arrives through partners who close deals you never see online, this structure fits. **Why does the first partner keep credit instead of the most recent one?** Channel partnerships are long-term relationships. The partner who originally established the customer relationship deserves ongoing credit, even if another partner interacts with that customer later. **Can a partner earn commission on deals that happen offline?** Yes. Alongside coupon tracking, you assign a partner's accounts or customers to them in the Siren admin. From then on every order those customers place credits the partner automatically, which covers relationships closed through phone calls, meetings, or trade events. **How do I set up partner-specific coupon codes?** Create a WooCommerce coupon for each channel partner and link it to their collaborator profile in Siren. When a customer uses that code at checkout, the partner is automatically credited. **Can I run this alongside an affiliate program?** Yes. Channel partner commissions and affiliate commissions operate as separate programs. If a customer uses a partner coupon code and also clicked an affiliate link, both programs can fire on the same transaction. **Does this require a specific Siren plan?** Basic coupon tracking is free. Crediting a partner for every order from the accounts assigned to them, ongoing and with no coupon, is lifetime attribution, which is a Siren Plus feature. ## Content Creator Profit Share Source: https://www.sirenaffiliates.com/recipes/content-creator-profit-share A monthly revenue-sharing distributor that rewards blog content creators proportionally based on the traffic their posts generate. Higher-performing posts earn a larger share of the pool. ## What This Recipe Does This recipe creates a monthly performance-weighted distributor that splits a portion of your store's revenue among blog content creators. Each collaborator's share of the pool is proportional to the traffic their bound posts generated during the distribution period. A writer whose posts attracted 60% of total tracked visits receives 60% of the pool. The distributor runs on a monthly cycle, calculating scores and distributing funds on the first day of each month. This gives content creators a predictable, recurring income stream tied directly to the value their writing delivers. ## Who It's For - **Multi-author WordPress sites** that publish content from several writers and want to reward them based on actual readership - **WooCommerce store owners** who rely on blog content to drive product sales and want writers invested in performance - **Content teams and editorial managers** looking for a transparent, automated way to split revenue without manual calculations ## How It Works When you apply this recipe, Siren creates a working monthly, performance-weighted distributor. The distributor uses the `performanceSharedPool` resolver, which divides the pool proportionally based on each collaborator's engagement score. Collaborators who drive more traffic earn a larger share. The recipe ships with a sensible default scoring setup so it runs the moment you apply it. It scores the `boundPostUsed` metric at 1 point, which fires each time a visitor reads a blog post bound to a collaborator, and it sizes the pool at 10% of qualifying revenue. You can adjust this in your Siren admin. Content-driven stores often raise the pool to a 15-25% range, and you can change the point value or set commission pool filters to specify which transaction types count toward the pool. Once configured, the system runs automatically. Siren tracks blog post visits throughout the month, tallies each collaborator's score, and on the first of the following month, distributes the accumulated pool proportionally. No manual intervention is needed after the initial setup. > What's the best way to split a monthly revenue pool among content creators based on each writer's traffic share? ### Program Snapshot - Best for: Multi-author blogs and WooCommerce stores with content teams - Main goal: Pay writers a recurring revenue share tied to readership - Partners involved: Blog writers and content creators bound to their posts - Actions tracked: Visits to bound blog posts via the boundPostUsed event - Rewards supported: Proportional share of a monthly revenue pool - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Distributor: Content Creator Profit Share ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Instructor Revenue Share (https://www.sirenaffiliates.com/recipes/instructor-revenue-share): The Instructor Revenue Share scores collaborators on student progress instead of readership: a finished course is worth 10 points and a completed lesson 1, and the monthly pool splits by those weighted totals. The Content Creator Profit Share counts each visit to a bound blog post as the scoring event. Pick the Instructor Revenue Share when you run courses on LifterLMS or LearnDash and want pay tied to what students complete rather than what readers view. - Management Incentive Plan (https://www.sirenaffiliates.com/recipes/management-incentive-plan): The Management Incentive Plan runs the same performance-shared pool on a quarterly clock, paying out on day one of the new quarter, and each manager is scored on the events their own role owns rather than on one shared metric. The Content Creator Profit Share distributes monthly and scores everyone on blog post visits alone. Choose the Management Incentive Plan when the earners are department heads measured on different channels and a three-month cycle matches your review calendar. ### Frequently Asked Questions **What is a content creator profit share?** A content creator profit share is an arrangement where writers receive a portion of the revenue their content helps generate. In this recipe, a percentage of monthly store revenue goes into a pool, and each writer's cut is sized by how much traffic their posts drew that month. A writer whose posts pulled 60% of tracked visits takes home 60% of the pool. **How does a content creator profit share work?** Each writer is bound to the posts they wrote, and visits to those posts add points to their score throughout the month. On the first of the next month, Siren sizes the pool from qualifying revenue since the last distribution and splits it by each writer's share of total points. There's nothing to calculate by hand once it's configured. **How does Siren know which blog posts belong to which collaborator?** Each collaborator is bound to the posts they authored. When a visitor reads one of those posts, the boundPostUsed event fires and credits the correct collaborator automatically. **What revenue counts toward the distribution pool?** You configure this after applying the recipe. In the commission pool filters, you choose which transaction types qualify. Most stores include WooCommerce order revenue, but you can narrow it to specific product categories or order types. **Can I adjust the revenue percentage after applying?** Yes. The revenue percentage is configured in your Siren admin and can be changed at any time. The new percentage takes effect for the next distribution cycle. **What happens if a collaborator's posts receive zero visits in a given month?** They earn zero points for that cycle and receive nothing from the pool. The performance-weighted model only rewards collaborators who generated measurable engagement. ## Cost-Per-Lead Campaign Source: https://www.sirenaffiliates.com/recipes/cost-per-lead-campaign A last-touch lead generation program where affiliates earn a flat fee for every form submission. The most recent affiliate interaction before the conversion gets credit, making this ideal for short sales cycles and time-sensitive campaigns. ## What This Recipe Does This recipe creates a single lead generation program with last-touch attribution. Affiliates earn a flat $15 bounty for every qualifying form submission they refer. When multiple affiliates interact with the same prospect, the affiliate whose engagement was most recent before the conversion earns the bounty. This structure is built for campaigns with short conversion windows where the final affiliate interaction is the strongest signal of who drove the lead. ## Who It's For - **Campaign managers** running time-boxed lead generation pushes where speed to conversion is the priority - **Businesses with short sales cycles** where leads convert within days, not weeks - **Teams that prefer last-touch attribution** because the affiliate closest to the conversion moment deserves credit ## How It Works When you apply this recipe, Siren creates a program that listens for collaborator form submissions. This event fires when a visitor referred by an affiliate submits a tracked form through a compatible plugin like Gravity Forms. Attribution uses last-touch logic. If multiple affiliates interacted with the same prospect over time, the affiliate whose engagement was most recent before the form submission gets credit. This "newest binding wins" approach rewards the affiliate who was actively driving the prospect at the conversion moment, not necessarily the one who found them first. This makes the program well-suited for short campaigns, seasonal promotions, and situations where your affiliates are actively competing to convert the same audience during a compressed timeframe. The affiliate doing the work right before the lead submits the form is the one who gets paid. Each qualifying form submission triggers a flat $15 commission. The payout is fixed and does not depend on any transaction amount, giving you a predictable cost per lead. > Is there a way to pay affiliates a flat fee per lead using last-touch (most-recent-click) attribution? ### Program Snapshot - Best for: Agencies and campaign managers running time-boxed lead pushes - Main goal: Generate qualified leads at a fixed, predictable cost - Partners involved: Affiliates driving prospects to your lead forms - Actions tracked: Form submissions from referred visitors via Gravity Forms - Rewards supported: Flat bounty per qualifying lead, $15 by default - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Cost-Per-Lead Program: $15.00 fixed per lead, newest engagement wins attribution, tracked via Form submissions ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Pay-Per-Lead Affiliate Program (https://www.sirenaffiliates.com/recipes/pay-per-lead-affiliate-program): The Pay-Per-Lead Affiliate Program runs on first-touch attribution, meaning whoever brought a prospect in first keeps the credit no matter who engages that person later, whereas the Cost-Per-Lead Campaign awards the newest touch. The Pay-Per-Lead default bounty is also higher: $25 per lead against the Cost-Per-Lead Campaign's $15. Pick the Pay-Per-Lead Affiliate Program when deals close weeks after the form fill and you want the introducer to hold credit through the whole cycle. ### Frequently Asked Questions **What is a cost per lead campaign?** A cost per lead campaign pays partners a fixed amount for every qualified lead they generate, usually a form submission, rather than a cut of an eventual sale. You know your acquisition cost before the campaign starts because every lead costs the same flat fee. That predictability makes it a common structure for agencies and short promotional pushes. **How does a cost per lead campaign work?** An affiliate sends a prospect to your site, the prospect submits a tracked form, and the affiliate earns a flat bounty for that lead. In this recipe the tracked event comes from Gravity Forms, and last-touch attribution decides who gets paid when several affiliates touched the same prospect. The bounty defaults to $15, and you can change it when you apply the recipe. **How is this different from the Pay-Per-Lead recipe?** Attribution. The Pay-Per-Lead recipe uses first-touch attribution, crediting the affiliate who originally introduced the lead. This recipe uses last-touch attribution, crediting the affiliate whose interaction was most recent before the form submission. **When should I use last-touch instead of first-touch attribution?** Last-touch works best for short sales cycles, time-sensitive campaigns, and situations where the final push matters more than initial discovery. If your leads typically convert within a few days, last-touch rewards the affiliate who was actively driving engagement at the moment of conversion. **Can I run this alongside a sales-based affiliate program?** Yes. This program tracks form submissions independently. If a lead later makes a purchase tracked by a separate sales-based program, both programs fire on their respective events without conflict. **What form plugins are supported?** Siren integrates with Gravity Forms for the collaboratorFormSubmitted engagement event. You configure which forms are tracked in your Siren settings. ## Coupon-Based Influencer Program Source: https://www.sirenaffiliates.com/recipes/coupon-based-influencer-program An influencer affiliate program that tracks commissions through unique coupon codes instead of referral links. Built for Instagram, TikTok, and YouTube creators who share discount codes in bios, stories, and video descriptions. ## What This Recipe Does This recipe creates an influencer program that tracks commissions exclusively through coupon codes. Each influencer gets a unique WooCommerce coupon code. When a customer enters that code at checkout, the influencer earns a 15% commission on the sale. There are no referral links involved. Tracking is based entirely on coupon usage, which makes this program ideal for social media creators who share codes in Instagram bios, TikTok videos, YouTube descriptions, and other places where clickable affiliate links are impractical or ignored. ## Who It's For - **Brands running influencer campaigns** where creators share discount codes with their audiences on social media - **Store owners who prefer coupon-based tracking** over link-based tracking for simplicity or branding reasons - **Social media marketers** managing multiple influencers across platforms like Instagram, TikTok, and YouTube ## How It Works When you apply this recipe, Siren creates a program that listens for a specific engagement event: a bound coupon being used at checkout. Each influencer in your program gets one or more WooCommerce coupon codes linked to their collaborator profile. When a customer applies one of those codes during checkout, Siren records the engagement and ties the resulting transaction to the influencer. The influencer then earns 15% of the order's line item total. Shipping, taxes, and fees are excluded from the calculation. If a customer uses coupon codes from two different influencers on the same order, the most recent coupon entry determines which influencer gets credit. In practice, WooCommerce typically allows only one coupon per order, so conflicts are rare. Because this program uses coupon tracking rather than link tracking, it works well alongside a separate link-based affiliate program. The two programs operate independently, and a single transaction can trigger commissions in both if the customer clicked a referral link and used a coupon code. > How do I run an influencer program where creators share coupon codes instead of referral links? ### Program Snapshot - Best for: Brands working with Instagram, TikTok, and YouTube creators - Main goal: Turn shared discount codes into tracked, commissionable sales - Partners involved: Influencers and content creators with assigned coupon codes - Actions tracked: Bound coupon codes used at checkout - Rewards supported: Percentage commission on every coupon-tracked sale - Starting point: Start free with Siren Lite ### What This Recipe Configures - Influencer Program: 15% percentage of transaction, newest engagement wins attribution, tracked via Coupon codes ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is an influencer affiliate program?** An influencer affiliate program pays social media creators a commission on the sales they drive, rather than a flat sponsorship fee alone. In this recipe, each creator gets a unique coupon code, and every order placed with that code earns them a 15% commission. Creator payouts stay tied directly to revenue. **How does an influencer affiliate program work?** The creator shares a coupon code with their audience in a bio, caption, or video description. When a customer applies that code at checkout, Siren credits the sale to the creator and calculates their commission. No clicks are involved, so attribution still works when the code spreads by screenshot or word of mouth. **How do I assign a coupon code to an influencer?** Create a WooCommerce coupon and link it to the influencer's collaborator profile in Siren. When a customer uses that code, Siren automatically credits the influencer. **Can an influencer have more than one coupon code?** Yes. You can assign multiple WooCommerce coupons to a single collaborator. This is useful for running different codes across different campaigns or platforms. **What if a customer uses a coupon code and also clicked a referral link from another program?** This program only tracks coupon code usage. If you also run a link-based program, both programs can fire on the same transaction, and each collaborator earns their respective commission. **Does the coupon code have to give the customer a discount?** No. You can create a WooCommerce coupon with a zero-dollar discount and use it purely for tracking. The code does not need to reduce the order total. ## Course Affiliate Program Source: https://www.sirenaffiliates.com/recipes/course-affiliate-program An affiliate program built for online course platforms and LMS sites. Affiliates share referral links to your course catalog and earn 30% commission on every enrolled student they refer. Designed for LifterLMS and LearnDash, and compatible with any LMS that runs on WooCommerce. ## What This Recipe Does This recipe creates a single affiliate program tailored for online course platforms. Affiliates share referral links to your course catalog. When a visitor clicks an affiliate's link and purchases a course, the affiliate earns a 30% commission on the sale. The configuration is straightforward, but the framing matters. This recipe speaks the language of course creators, LMS operators, and education marketplaces. The higher default commission rate reflects the reality of digital product economics, where margins are high and affiliate payouts can be generous without cutting into profitability. ## Who It's For - **Course creators and LMS operators** who want to grow enrollments through referral partnerships - **LifterLMS and LearnDash site owners** looking to add an affiliate channel alongside their existing course catalog - **Online education platforms** building a Udemy-style, Coursera-style, or Teachable-style marketplace that rewards promoters for driving student sign-ups ## How It Works When you apply this recipe, Siren creates a program that tracks referred site visits. Each affiliate gets a unique referral link to your site. When a prospective student clicks that link and lands on your course catalog, Siren records the visit and ties it to the referring affiliate. If that visitor goes on to purchase a course, the affiliate earns 30% of the transaction's line item total. Attribution follows a "newest engagement wins" model. If a student clicks links from two different affiliates before enrolling, the most recent referral determines who gets paid. This keeps the system simple and gives affiliates a clear incentive to stay active in promoting your courses. This program is designed to run alongside instructor royalty programs. Because the affiliate program and a course creator incentive program are separate, ungrouped programs, they stack naturally. A single course purchase can pay out both a 30% affiliate commission and a separate royalty to the course creator. The two do not compete or interfere with each other. Commissions are calculated on line items only, so taxes, fees, and any platform surcharges are excluded. This gives you a clean cost basis for your affiliate payouts. > How do I launch an affiliate program for my online course catalog on LifterLMS or LearnDash? ### Program Snapshot - Best for: Course creators and LMS operators selling through WooCommerce - Main goal: Grow course enrollments through affiliate referrals - Partners involved: Affiliates promoting your course catalog - Actions tracked: Referred site visits and course enrollment purchases - Rewards supported: 30% commission on each referred course sale - Starting point: Start free with Siren Lite ### What This Recipe Configures - Course Affiliate Program: 30% percentage of transaction, newest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Basic Affiliate Program (https://www.sirenaffiliates.com/recipes/basic-affiliate-program): The Basic Affiliate Program is the general-store version of the Course Affiliate Program's machinery, with the same referred-visit tracking, most-recent-link credit, and line item percentage math. The Basic Affiliate Program defaults to 20% on ordinary WooCommerce products rather than the 30% course rate. Pick the Basic Affiliate Program when you run a standard WooCommerce store rather than a course catalog and a 20% default fits your product margins. - First-Touch Referral Program (https://www.sirenaffiliates.com/recipes/first-touch-referral-program): Under the First-Touch Referral Program, whoever referred a customer first wins the payout, the reverse of the Course Affiliate Program's newest-referral rule. It also defaults to a 20% commission where the course recipe pays 30%. Lean toward the First-Touch Referral Program if early referrers must never lose credit to whoever happens to click last. ### Frequently Asked Questions **What is a course affiliate program?** A course affiliate program pays outside promoters a commission for each student they send to your online courses. Affiliates share referral links to your catalog, and when someone clicks through and enrolls, the affiliate earns a cut of the sale. For online courses that cut is usually a percentage of the enrollment price rather than a flat fee. **How does a course affiliate program work?** An affiliate shares their unique link, a prospective student clicks it, and the program records that visit. When the student buys a course, the sale is matched back to the affiliate and a commission is calculated on the purchase. In this recipe, Siren does the matching on your own WordPress site and pays the most recent referrer 30% of the course's line item total. **Why is the default commission rate 30% instead of the typical 20%?** Digital products like online courses carry near-zero marginal cost per sale. Higher commission rates are standard in this space because there is no inventory, shipping, or manufacturing cost eating into margins. **Can I run this alongside a course creator royalty program?** Yes. Affiliate commissions and instructor royalties are separate programs. They stack on the same transaction because they are not grouped together. An affiliate earns their commission and the course creator earns their royalty independently. **Does this work with LifterLMS and LearnDash?** Yes. Siren integrates natively with both LifterLMS and LearnDash. As long as your courses are sold through WooCommerce, affiliate commissions are calculated automatically on each enrollment purchase. **What if a student clicks links from two different affiliates before enrolling?** The most recent referral wins. Siren credits the affiliate whose link the student clicked last before completing the purchase. ## Course Creator Royalty Program Source: https://www.sirenaffiliates.com/recipes/course-creator-royalty-program A royalty program for course creators and instructors. When a student purchases a course, the instructor who created it automatically earns a percentage of the sale. No referral links or manual tracking required. ## What This Recipe Does This recipe creates a royalty program where course instructors earn a percentage of every sale of their courses. When a student buys a course, Siren detects the product in the order, identifies the instructor who owns it, and credits them with a 50% royalty. No referral links, no coupon codes, no manual attribution. The tracking is fully automatic based on product ownership. This is the standard revenue model used by course platforms like Udemy and Skillshare, rebuilt for WordPress. If you run an online education site with multiple instructors and want each creator to earn a fair share of the revenue their courses generate, this is the recipe to start with. ## Who It's For - **Online course platform operators** who want to pay instructors automatically when their courses sell - **LifterLMS or LearnDash site owners** looking for a simple, automated royalty system tied to course sales - **Education entrepreneurs** building a multi-instructor marketplace on WordPress and WooCommerce ## How It Works When you apply this recipe, Siren creates a program that tracks a single engagement type: collaborator product sold. This event fires whenever a WooCommerce order contains a product that has been assigned to a collaborator. In this case, the collaborator is the course instructor and the product is the course. You set up the relationship once: assign each course to its instructor in Siren's owned products settings. From that point forward, tracking is hands-off. A student buys Introduction to Photography, Siren sees that the product belongs to the Photography instructor, and that instructor earns 50% of the sale price. The resolver is "newest binding wins," which in this context is straightforward. Each product has a clear owner, so there is no competition for attribution. One course sold equals one royalty credited to the instructor who created it. If a single order contains courses from multiple instructors, each instructor earns their royalty independently. A student buying three courses from three different creators generates three separate commissions, one per instructor. The royalty rate applies to each course's line item value, not the order total, so each instructor's payout is based on the price of their specific course. > How do I automatically pay course creators a royalty every time one of their courses sells? ### Program Snapshot - Best for: Multi-instructor course platforms on WordPress and WooCommerce - Main goal: Pay creators a share of every course sale automatically - Partners involved: Course instructors added to Siren as collaborators - Actions tracked: Course sales detected through product ownership - Rewards supported: Percentage royalty on each course's sale price - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Course Creator Royalties: 50% percentage of transaction, newest engagement wins attribution, tracked via Owned product sales ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Marketplace Vendor Commission (https://www.sirenaffiliates.com/recipes/marketplace-vendor-commission): The Marketplace Vendor Commission program runs on the every binding wins resolver, so an order mixing products from several vendors pays all of them at once, and its 70% default hands the majority of each sale to the vendor rather than the platform. Pick it when vendors supply and fulfill their own products and your platform keeps the minority share of each sale. - Product Royalty Program (https://www.sirenaffiliates.com/recipes/product-royalty-program): The Product Royalty Program shares the Course Creator Royalty Program's ownership tracking, a collaborator product sold event resolved by newest binding wins, but it defaults to a 40% royalty and treats any WooCommerce product as the owned work, not just courses. Reach for it when the creators on your store make designs, downloads, or physical goods rather than courses. ### Frequently Asked Questions **What is a course creator royalty program?** A course creator royalty program pays instructors a percentage of revenue whenever the courses they created sell. It's the model Udemy and Skillshare run: the platform handles the sale, and the creator earns a share of each purchase of their own work. Unlike an affiliate program, nobody has to promote a link to get paid. **How does a course creator royalty program work?** Each course gets assigned to the instructor who made it. When a student buys, the system checks which courses are in the order, finds their owners, and credits each one their share. In Siren that share defaults to 50% of the course's sale price, applied per line item, so two instructors in one order each earn on their own course. **How does Siren know which instructor to pay?** You assign each course (WooCommerce product) to its instructor using Siren's owned products feature. When that course sells, Siren automatically credits the instructor. **What happens if a course has multiple instructors?** Assign the course to each instructor. When the course sells, every instructor who owns it earns the royalty independently. **Does this work with LifterLMS and LearnDash?** Yes. Both plugins sell courses through WooCommerce. As long as the course is a WooCommerce product and assigned to an instructor in Siren, royalties are tracked automatically. **What Siren plan do I need?** This recipe requires the Essentials tier. Product ownership tracking and the collaboratorProductSold engagement type are Essentials features. ## Curated Content Partner Program Source: https://www.sirenaffiliates.com/recipes/curated-content-partner-program Launch an invite-only content partner program on WooCommerce. A curated roster of creators each own a dedicated article, and they earn a commission whenever a reader buys after reading their piece. No affiliate links, no public sign-up. Only the partners you invite can earn. ## What This Recipe Does This recipe launches an invite-only content partner program. You hand-pick a roster of creators, give each one a dedicated piece of content on your site, and pay them a commission whenever a reader buys after reading their piece. There are no affiliate links to share and no clicks to chase. The content readers actually read is what earns the partner credit. The program is invite-only because Siren treats program membership as the gate. A collaborator earns from a program only once you have added them to it, and this recipe adds your named partners as it applies, so that short list is everyone who can earn. Someone you never invited collects nothing, even if they happen to author content a reader views. That makes this the right fit when you want a curated set of trusted partners rather than a public program anyone can sign up for. It is a natural way to turn editorial collaborations and sponsored content into performance-based partnerships, where partners are rewarded for the sales their work drives instead of a flat fee paid up front. ## Who It's For - **Brands** that want each invited creator to own a signature article and earn on the sales it drives - **Editorial and sponsored-content teams** that prefer to pay a short list of named writers a commission rather than a flat placement fee - **Stores** that want a private, hand-picked partner program with no affiliate links to manage and no public sign-up ## How It Works When you apply this recipe, Siren sets up a single program that pays your partners based on the content readers consume. Each partner is credited through the content they authored. When a reader visits that piece and later completes a purchase, Siren credits the partner whose content they read and pays them a commission on the sale. No link, no coupon code, and no click tracking required. The post itself is the tracking. Siren ties the credit to a reader viewing the partner's post, so there is nothing for the partner to share or for you to maintain. The [blog post visits](/documentation/general/blog-post-visited) guide covers how that view is recorded and how long a read stays eligible to earn. If a reader took in two partners' posts before buying, the most recently read one earns the commission. The program is closed by design. Eligibility never extends past the partners you have added to the program, so the question of who can earn has the same answer as who you invited. This is what separates an invite-only partner program from an open one. The same content-based reward is in play, but here it is reserved for exactly the creators you chose. Credit follows authorship. Siren looks at the WordPress author of the post a reader visited and matches that author's user account to a partner, so each partner earns on the pieces published under their account. When an article should credit a guest creator, set the post's author to that partner's linked user, keeping in mind that the visible byline follows the author field. Connecting partners to their user accounts is covered in [managing collaborators](/documentation/getting-started/managing-collaborators-affiliates). Commissions are calculated on the product subtotal with discounts applied, so each payout reflects the real revenue the order produced. ## The Partners Are Examples to Replace The recipe ships with three example partners so you can see how a curated slate is shaped. They are placeholders, not real people. Before you launch, swap them out for your own invited creators, using each partner's real email address. You can list as many partners as you need, and the recipe adds every one of them to the program as it applies. From there, adding a partner grants eligibility and removing one withdraws it, so you stay in full control of who is in the program at any time. ## When to Use This Reach for this recipe when you want a small, trusted group of creators and you want to reward them for results rather than a flat fee. It shines for paid content partnerships, expert guest articles, and sponsored editorial where each partner owns a signature piece and earns on the sales it influences. If instead you want a program that keeps taking on new creators as your contributor pool grows, the Blog Content Program recipe is built around that growing list. Keeping the program closed costs nothing beyond the recipe itself. Content-based attribution does the tracking, and because earning is limited to the partners added to the program, the invite-only behavior is simply how Siren programs work. If the same creators eventually need to power several programs at once, a [collaborator group](/documentation/general/what-are-collaborator-groups) can carry one shared list across all of them, though nothing in this recipe requires it. > How do I run an invite-only partner program where hand-picked creators earn commission when readers buy after reading their content, with no affiliate links? ### Program Snapshot - Best for: Brands paying a hand-picked set of creators for editorial content - Main goal: Turn sponsored articles into commission-based partnerships - Partners involved: Invited creators, each owning one piece of content on your site - Actions tracked: Reads of each partner's authored post, carried through to purchase - Rewards supported: Percentage commission on sales the partner's content influenced - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Curated Content Partner Program: 20% percentage of transaction, newest engagement wins attribution, tracked via Blog post visits ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Blog Content Program (https://www.sirenaffiliates.com/recipes/blog-content-program): The Blog Content Program ships the same read-then-buy attribution with no partners attached, built for a contributor pool you keep enrolling into at a 15 percent default. The Curated Content Partner Program arrives with its partner slate named in the recipe, each one added to the program on apply, and defaults to 20 percent. Reach for the Blog Content Program when the writer list keeps growing and you would rather enroll contributors as they come than name a fixed slate up front. - Invite-Only Partner Program (https://www.sirenaffiliates.com/recipes/reusable-partner-roster): Coupon codes do the tracking in the Invite-Only Partner Program: a roster member earns 15 percent when an order redeems their bound code, and nothing fires on content views. The Curated Content Partner Program pays 20 percent on purchases that follow a read of the partner's article. Choose the Invite-Only Partner Program where partners promote off-site and a shareable code is the practical way to tie sales back to them. ### Frequently Asked Questions **What is an invite-only content partner program?** It's a closed partnership where only creators you've invited can earn commission from the sales their content drives. Each partner owns a piece of content on your site, and a reader who buys after reading it earns that partner a percentage of the order. A creator you never invited earns nothing, no matter whose content a reader views. **How does an invite-only content partner program work?** Three things happen in sequence: a reader visits a partner's article, the read is recorded, and when that reader completes a purchase the partner collects a commission on it. The invite-only part is built into the program itself, since a Siren program only ever pays the collaborators added to it. There are no affiliate links or codes involved, the content itself carries the attribution. **Can I change the commission rate after launching?** Yes. The default pays each partner 20% of every sale their content influenced, and you can edit that rate in your Siren admin at any time. You can set the rate before you launch the program or adjust it later as your partnerships grow. **How is this different from an open program any creator can join?** An open content program keeps enrolling new creators as your contributor pool grows. This one starts from a fixed, hand-picked slate: the partners named in the recipe are added to the program when you apply it, and they remain the only people who can earn from it until you decide otherwise. It is the right fit when you want a short list of trusted partners rather than a payout that grows with every new contributor. **Do partners need affiliate links?** No. There are no links to share and no clicks to track. Each partner owns a piece of content on your site, and when a reader buys after reading that piece, the partner earns. The content itself does the attribution, which makes this a clean fit for editorial and sponsored-content partnerships. **How do I add or remove partners over time?** The recipe ships with three example partners so you can see the shape of a curated list. Replace them with your own invited creators, using each partner's real email address, and each one joins the program the moment the recipe applies. Later on, adding a partner to the program grants eligibility and removing them ends it, so you stay in full control. **Do I need WooCommerce for this to work?** Yes. Siren calculates commissions from WooCommerce orders, so WooCommerce must be installed and active. The program rewards a partner when a reader buys after reading the partner's content. **Which Siren plan do I need?** The plan badge at the top of this page comes straight from the recipe, and the feature that sets it is blog post tracking, the engine behind content-based attribution. The invite-only side asks for nothing extra. Every Siren program starts closed on every plan, so restricting earning to your chosen partners is the default, not an upgrade. **How and when do partners get paid, and what about refunds?** Siren tracks what each partner has earned, and you pay them on your own schedule. Partners can see their earnings in the collaborator dashboard. Because credit follows real sales, a refunded or cancelled order is handled by Siren's standard payout rules. See the how to pay collaborators and collaborator dashboard guides, linked below. ## Customer Rewards Program Source: https://www.sirenaffiliates.com/recipes/customer-rewards-program A loyalty program where customers earn a flat credit on every purchase. No referral links needed because rewards are tracked automatically by customer. ## What This Recipe Does This recipe creates a single program that rewards customers with a flat $5.00 of store credit every time they make a purchase. Unlike a referral-based affiliate program, this one uses manual attribution. You enroll customers as collaborators and they earn rewards on their own purchases. The reward is paid in store credit, a custom currency the customer spends on a future order, rather than a cash payout. This is a loyalty and retention play, not a referral play. Choose this recipe when you want to reward customers for buying, not for promoting. ## Who It's For - **Store owners** who want to incentivize repeat purchases without asking customers to share links - **Subscription businesses** looking to reward customers for renewals - **Anyone building a loyalty program** as a complement to or alternative to a traditional affiliate program ## How It Works Your customers earn store credit just by shopping with you. You add each customer to the program as a collaborator, and when they make a purchase, you or an automation attributes the transaction to them. They earn a flat $5.00 credit per order, regardless of how much they spent. This approach works well for stores that want to build loyalty without the complexity of referral tracking. There are no links to share, no codes to remember. Customers just buy and earn. The reward amount is the same whether someone places a $20 order or a $200 order, which keeps the program easy to understand and communicate. Attribution uses a "newest engagement wins" model, but since each customer's engagement is unique to them, this effectively means each customer earns independently on their own purchases. ## Automating store credit at checkout The reward is paid in a custom store-credit currency rather than cash, so it stays inside your store and pulls customers back for another order. Defining that currency and rewarding customers in it are free-tier features, and on the free tier you issue each balance by hand. To make it automatic, add a credit autofulfiller. It turns each earned balance into a coupon that applies at the customer's next checkout, so the credit redeems itself with no manual step. The binding is a Siren Essentials feature, which keeps the program free to run and makes the automation the upgrade. Add it by binding the store-credit currency to the built-in `credit` fulfillment method: ```json { "autofulfillers": { "storeCreditAuto": { "currency": "STORE_CREDIT", "method": "credit" } } } ``` > How do I reward customers with a credit on every purchase they make, no referral link required? ### Program Snapshot - Best for: Stores and subscription businesses rewarding repeat buyers - Main goal: Keep existing customers coming back with credit on every order - Partners involved: Your own customers, enrolled as collaborators - Actions tracked: Purchases attributed to each enrolled customer, manually or via automation - Rewards supported: Flat store credit per purchase, spendable at their next checkout - Starting point: Start free with Siren Lite ### What This Recipe Configures - Customer Rewards: $5.00 fixed per transaction, newest engagement wins attribution, tracked via Manual attribution ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Automatic credit fulfillment (Siren Essentials): Issue the store credit or points a customer earns automatically, as a coupon that applies at their next checkout, instead of granting it by hand. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a customer rewards program?** A customer rewards program gives your existing customers something back for continuing to buy from you. In this recipe, that's a flat $5.00 credit on every purchase they make. It's a retention tool rather than an acquisition one: you're thanking people for coming back, not paying outsiders to send you traffic. **How does a customer rewards program work?** You enroll customers in the program, and from then on each purchase they make earns a defined reward. There are no referral links here: a purchase gets attributed to the customer who placed it, either manually or through an automation, and Siren records the $5.00 credit. Order size doesn't change the reward, so customers always know exactly what they'll get back. **Why is the incentive amount 500 instead of 5 in the JSON?** The amount is stored in the currency's smallest unit. Store credit is valued one to one with your store currency, so 500 means $5.00 of credit. When adjusting the reward, use that smallest unit (for example, 1000 for $10.00 of credit). **Does this require a specific Siren plan?** No, not to start. The store-credit currency and rewarding customers in it are free-tier features, and you can issue each balance by hand. Automating it, so the credit becomes a coupon that applies at the customer's next checkout, uses a credit autofulfiller, which is a Siren Essentials feature. **Do customers need to share referral links?** No. Customers earn rewards on their own purchases. There are no referral links involved. Attribution is handled manually or through automation. **Can I run this alongside an affiliate program?** Yes, and many stores do. Your customers earn rewards on their purchases while your affiliates earn commissions on referred sales. The two programs operate independently. **How do I enroll customers in the program?** Add them as collaborators, either manually through the Siren admin or through a registration form. Once enrolled, they earn rewards on every purchase. ## Dealer Sell-Through Commission Program Source: https://www.sirenaffiliates.com/recipes/dealer-sell-through-commission A commission tracking program for manufacturers and wholesalers paying dealer reps on sell-through. Retail sales from each dealer's monthly report are entered or imported as orders, attributed in bulk to the rep who made them, and every rep collects a set percentage of what's credited to them. Only the reps you add to the program can earn. ## What This Recipe Does This recipe installs commission tracking for dealer reps paid on sell-through. The sales happen at your dealers, reach you as a monthly report, and get entered or imported into your store as orders. You then attribute each sale to the rep who made it, and that rep earns 5% of the sale's line item total. Nothing in the program watches customers. There are no links to hand out and no codes to redeem, because the buyer purchased from a dealer's counter, not from your website. Attribution is a back-office action you perform in bulk once a month, and the commission math follows from it automatically. Eligibility takes care of itself. Siren pays a collaborator only when they have been placed on a program, and applying this recipe places each of your named reps on it. Three sample reps ship with the recipe to show the shape of the roster. Replace them with your real dealer reps before applying. ## Who It's For - **Manufacturers with dealer networks** whose reps are paid on retail sales that arrive as numbers in a report, never as orders in a system - **Wholesalers and master distributors** who owe reps a cut of what their accounts actually sold through last month - **Whoever currently rebuilds the monthly report into a commission spreadsheet** and wants that job gone ## How It Works The engine under this recipe is Siren's manual attribution trigger. When you attribute a transaction to a rep, Siren raises a Manual Attribution event, finds the active programs that rep belongs to with the manual trigger enabled, and creates an engagement for each. The engagement becomes a conversion, the conversion produces an obligation, and the obligation carries the rep's 5% of the sale's line items. Because this recipe enables only the manual trigger, no customer behavior can ever fire the program. The [manual attribution](/documentation/general/manual-attribution) doc explains the event chain, and [manually attribute a transaction](/documentation/getting-started/manually-attribute-a-transaction) walks through doing it. A month looks like this in practice. The dealer report arrives. You enter or import its sales as WooCommerce orders, or push them in through the REST API if a script suits you better. On the Transactions screen, you filter to the new orders, select the ones belonging to one rep, and run the bulk attribution action. Repeat for the next rep until the report is exhausted. A fifty-line report becomes a handful of selections rather than fifty individual entries. Mistakes are cheap to fix. Crediting the wrong rep leaves a pending conversion you can reject on the Conversions screen, and rejecting it pulls the commission back with it. Attribute the sale to the right rep and the month is clean again. Territory adjustments and mid-month account changes get handled the same way. Commission is figured strictly on line items, so shipping and tax never enter the calculation. The 5% default adjusts to whatever rate your rep agreements name. When different reps earn different rates, run a separate program per rate and attribute into the right one. ## The Last Month You Rebuild the Report by Hand Every operation paying reps on retail sales knows the ritual. The dealer's numbers land as a CSV or a PDF, and someone opens last month's spreadsheet, copies it forward, keys in the new rows, matches every line to a rep from memory or a lookup tab, drags the percentage formula down, and then spot-checks the totals because one mistyped row in March cost a rep real money. The report took the dealer minutes to send and takes your team the better part of a day to turn into payable commissions. This recipe keeps the report and deletes the rebuild. The numbers go in once, as orders. Matching lines to reps becomes the bulk attribution step, done in batches instead of row by row. The percentage lives in the program configuration, where nobody can fat-finger it, and the totals on each rep's record are computed from the attributed sales rather than maintained beside them. When a rep disputes a sale, you look at the transaction together, and if they're right you reject the bad credit and assign the sale where it belongs. For dealers who send the same format every month, the REST API closes the loop further: a small import script can create the transactions and attribute them in the same pass, which turns report day into a review instead of a data entry shift. The spreadsheet does not get faster under this recipe. It stops existing. > How do I pay dealer reps commission on sell-through sales that only reach me as a monthly report from each dealer? ### Program Snapshot - Best for: Manufacturers and wholesalers paying dealer reps on sell-through - Main goal: Turn each dealer's monthly report into paid rep commissions - Partners involved: Dealer reps you add to the program by name - Actions tracked: Reported retail sales, attributed to reps in monthly batches - Rewards supported: Set percentage of each credited sale, line items only - Starting point: Start free with Siren Lite ### What This Recipe Configures - Dealer Sell-Through Commission: 5% percentage of transaction, newest engagement wins attribution, tracked via Manual attribution ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Business Partner Revenue Share (https://www.sirenaffiliates.com/recipes/business-partner-revenue-share): The Business Partner Revenue Share credits orders your store already recorded, assigning each one to a named partner as the deal lands, with automation available for recurring arrangements. The Dealer Sell-Through Commission Program starts outside the store entirely: retail sales arrive on a dealer's report, get entered or imported, and are attributed to reps in one monthly pass. Stay with the Business Partner Revenue Share when sales flow through your own checkout and each one belongs to a partner you can name the day it happens. - Channel Partner Program (https://www.sirenaffiliates.com/recipes/channel-partner-program): Crediting in the Channel Partner Program happens at checkout: a reseller's bound coupon code earns them the sale automatically, the oldest binding keeps the account, and the default rate is 15 percent. The Dealer Sell-Through Commission Program has no codes at all, and every reported retail sale is credited by hand at 5 percent. Send partners to the Channel Partner Program when they close orders at your storefront and a bound code can name the earner without anyone reading a report. - Sales Override Commission Program (https://www.sirenaffiliates.com/recipes/tiered-sales-override-program): Two payouts ride one coupon-tracked deal in the Sales Override Commission Program, with the closing rep keeping a percentage while a fixed $50 pool is shared out across the managers ranked over them. The Dealer Sell-Through Commission Program is flat, paying one rep per attributed sale with nothing rolling up the chain. Adopt the Sales Override Commission Program when regional managers should take an override on each deal closed beneath them. ### Frequently Asked Questions **What is a sell-through commission program?** A sell-through commission program pays a rep on the retail sales a dealer makes to end customers, not on the wholesale order the dealer placed with you. The dealer reports what actually sold, usually monthly, and the rep who drove those sales earns a percentage of each one. It rewards product moving off the dealer's shelf rather than product sitting in the dealer's stockroom. **Who should use a sell-through commission program?** Manufacturers and wholesalers whose products sell through dealer networks and whose reps are paid on what moved at retail. If your month ends with a sales report from every dealer and someone rebuilding those numbers into a commission spreadsheet, this structure is built to replace that spreadsheet. **How does the monthly attribution actually work?** Enter or import the report's orders into WooCommerce, or create the transactions over Siren's REST API. Then select a rep's sales on the Transactions screen and run the bulk action that attributes them to that rep. Siren credits every sale in the batch, evaluates the program, and figures the commission, so one pass per rep covers the whole month. **What happens when a sale on the report is returned?** If the return surfaces before you've entered the month, leave that line out of the import and it never becomes a commission. If the order is already in your store, refund it there and Siren cancels the transaction, rejects the conversion, and rejects the obligation along with it, as long as that obligation hasn't already been paid out. **Can my reps see what they've earned?** Yes. Each rep gets access to the collaborator dashboard, where their attributed sales and running balance are visible without anyone forwarding a spreadsheet. Questions about a specific sale turn into a lookup instead of an email thread. **Do reps need tracking links or coupon codes?** No. The customer bought from a dealer, not from your site, so there's no click or checkout event to capture. The only trigger this program listens for is you attributing a transaction to a rep, which is why the report-driven workflow fits it. **What if two reps split a dealer's sales?** Attribute each rep's portion of the report separately, since every sale should carry exactly one rep. For splits that follow product lines or territories within one dealer, filter the orders accordingly before running each bulk attribution. If a sale lands on the wrong rep, reject that pending credit on the Conversions screen and attribute the sale to the right rep. **Can a regional manager take an override on top of the rep's commission?** Not in this recipe. It pays one rep per attributed sale and nothing rolls upward. The Sales Override Commission Program handles that chain model, ranking each closing rep's line of managers and splitting an override pool across them, so reach for that recipe when managers need a cut. **Do these sales have to go through WooCommerce checkout?** No customer ever checks out for these orders. You key them in or import them, and WooCommerce simply holds them as order records so Siren can calculate commission against the line items. You can also skip WooCommerce entirely and create the transactions directly through Siren's REST API. **What stops someone outside my roster from earning?** Eligibility in Siren starts empty: no collaborator earns from a program they haven't been placed on, and applying this recipe is what puts your named reps on it. Swap the three sample reps for your real ones first, then add or remove reps inside Siren as your roster changes. ## Donation Per Purchase Source: https://www.sirenaffiliates.com/recipes/donation-per-purchase An owned-products donation model where partner organizations earn a flat donation for every sale of their assigned products. Ideal for cause-driven commerce where every purchase supports a specific organization. ## What This Recipe Does This recipe creates a program where partner organizations are assigned specific products. Every time one of those products sells, the organization earns a flat $2.00 donation. The tracking is automatic: Siren detects the product in the order and attributes the commission to the organization that owns it. This is not a referral program. Organizations do not need to share links or drive traffic. They earn simply by having their products sell. Choose this recipe when your business model ties specific products to specific causes or partners and you want that relationship tracked and compensated automatically. ## Who It's For - **Cause-driven e-commerce stores** that want to donate a fixed amount per sale to partner nonprofits or community organizations - **Marketplace operators** who assign products to vendors and pay a flat per-product commission - **Mission-driven businesses** where specific products fund specific causes, and the store wants that tracked and automated ## How It Works You add each partner organization as a collaborator in the program and assign them one or more products using Siren's owned products feature. From that point on, everything is automatic. When a customer purchases one of those products, Siren detects the product in the order, matches it to the organization that owns it, and credits them with a $2.00 donation per unit sold. The attribution model here is "every engagement wins," which is different from the referral-based recipes. In referral programs, affiliates compete for credit on the same sale. Here, there is no competition. If a single order contains products owned by three different organizations, all three earn their respective donations independently. Every product-organization pair triggers its own payout. This makes the donation model clean and predictable: one product sold equals one donation credited, regardless of what else is in the cart. > How do I donate to a partner organization for every sale of their sponsored products? ### Program Snapshot - Best for: Cause-driven stores and marketplaces pairing products with partner organizations - Main goal: Route a fixed, auditable donation to partners on every qualifying sale - Partners involved: Nonprofits, community organizations, and vendors who own assigned products - Actions tracked: Sales of partner-owned products, detected per order line item - Rewards supported: Flat donation per unit sold - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Product Donation Program: $2.00 fixed per product, every engagement wins attribution, tracked via Owned product sales ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a donation per purchase program?** A donation per purchase program commits a fixed dollar amount to a partner organization every time one of their designated products sells. Instead of donating a vague percentage of profits once a year, the pledge is tied to individual sales and tracked automatically, so you and the partner both see exactly what each purchase contributed. **How does a donation per purchase program work?** You assign products to each partner organization, and the system watches your orders for those products. When one sells, a fixed donation is credited to the owning organization, the same way a commission would be. There's no referral link involved: the donation triggers on the sale itself. **Why is the incentive amount 200 instead of 2 in the JSON?** The amount is stored in cents. 200 means $2.00. To donate $5.00 per product, set the amount to 500. **Can I set different donation amounts for different products?** Not within a single program. If you need different amounts for different products, create multiple donation programs with their own rates and assign products accordingly. **Do organizations need to do anything to earn donations?** No. Once you assign products to an organization, tracking is fully automatic. Siren detects the product in the order and credits the donation. **What Siren plan do I need?** This recipe requires the Essentials tier. Fixed-per-product incentives and product ownership are Essentials features. ## First-Touch Referral Program Source: https://www.sirenaffiliates.com/recipes/first-touch-referral-program An affiliate program using a first-touch attribution model that credits the first affiliate who referred the customer, not the last. Built for longer sales cycles where the person who originally introduced a buyer deserves the commission. ## What This Recipe Does This recipe creates an affiliate program that uses first-touch attribution. When multiple affiliates refer the same customer over time, the affiliate who sent the customer to your store first earns the commission. Later referrals from other affiliates do not override the original. Most affiliate programs default to last-touch attribution, where the most recent referral wins. This recipe takes the opposite approach. It rewards the affiliate who originally introduced the customer to your store, even if the customer later encounters other affiliate links before purchasing. ## Who It's For - **Businesses with longer sales cycles** where customers visit multiple times over days or weeks before buying - **Store owners who value lead generation** and want affiliates focused on bringing in new potential customers rather than re-engaging existing ones - **High-ticket or considered-purchase sellers** (courses, memberships, premium products) where the initial introduction matters more than the final click ## How It Works When you apply this recipe, Siren creates a program that tracks referred site visits and uses the "oldest binding wins" resolver to determine attribution. Here is what that means in practice. When a visitor first arrives at your store through an affiliate's link, Siren records that referral and binds the visitor to that affiliate. If the same visitor later clicks a different affiliate's link, Siren still keeps the original binding. The first affiliate retains credit. When the visitor eventually makes a purchase, the commission goes to the affiliate who made the original referral. The affiliate earns 20% of the order's line item total. This model works well for products with longer consideration periods. If you sell online courses, annual memberships, or premium goods, your customers likely visit several times before committing. First-touch attribution ensures that the affiliate who introduced the customer gets rewarded, even if weeks pass between the initial referral and the purchase. Shipping, taxes, and fees are excluded from the commission calculation. Only line item totals are used. > What's the best way to give credit to the first affiliate who introduced the buyer, not the last click? ### Program Snapshot - Best for: Stores selling considered purchases with long research periods - Main goal: Reward the affiliate who first brings each customer in - Partners involved: Affiliates who introduce new buyers early in their research - Actions tracked: Referred site visits and purchases, credited to the original referrer - Rewards supported: Percentage commission on referred sales, 20% by default - Starting point: Start free with Siren Lite ### What This Recipe Configures - Affiliate Program: 20% percentage of transaction, oldest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Basic Affiliate Program (https://www.sirenaffiliates.com/recipes/basic-affiliate-program): The Basic Affiliate Program resolves competing referrals the opposite way from the First-Touch Referral Program. The last affiliate link a customer clicks takes the 20% commission, while the First-Touch Referral Program leaves credit with whoever referred that customer first. Use the Basic Affiliate Program for short sales cycles where the closing click and the original introduction rarely diverge. - Course Affiliate Program (https://www.sirenaffiliates.com/recipes/course-affiliate-program): The Course Affiliate Program pays whichever affiliate referred the buyer most recently rather than the affiliate who made the earliest introduction, and it carries a 30% default on each enrollment sale, aimed at LifterLMS and LearnDash sites. Choose the Course Affiliate Program if you run an LMS catalog and want recent, active promotion rewarded at a rate suited to digital margins. ### Frequently Asked Questions **What is a first-touch referral program?** A first-touch referral program credits each sale to the first affiliate who introduced the customer, not the last one whose link was clicked before checkout. The original referral is recorded when the visitor first arrives, and that record decides who earns the commission whenever the purchase happens. It rewards affiliates for discovering new buyers rather than intercepting ones already headed to your store. **Who should use a first-touch referral program?** Stores whose customers research before buying: courses, memberships, and high-ticket products are the classic cases. If your typical buyer visits several times over days or weeks, first-touch makes sure the affiliate who started that journey gets paid. It matters less for impulse purchases, where the first and last referral are usually the same affiliate anyway. **What happens if a customer clicks multiple affiliate links before buying?** The first affiliate who referred the customer gets credit. Even if the customer later clicks a different affiliate's link, the original referrer earns the commission. **How is this different from the Basic Affiliate Program recipe?** The Basic Affiliate Program uses last-touch attribution, where the most recent referral wins. This recipe flips that logic so the oldest referral wins instead. **Can I switch from first-touch to last-touch later?** Yes. You can change the incentive resolver type in Siren's program settings at any time. Existing commissions are not affected, but new transactions will use the updated attribution model. **Is first-touch attribution better than last-touch?** Neither is universally better. First-touch rewards the affiliate who discovered the customer. Last-touch rewards the affiliate who closed the sale. Choose based on what behavior you want to incentivize in your affiliates. ## Fixed-Rate Affiliate Program Source: https://www.sirenaffiliates.com/recipes/fixed-rate-affiliate-program An affiliate program that pays a flat dollar amount per referred sale instead of a percentage. Affiliates earn a predictable $10 commission on every transaction they refer, regardless of order size. ## What This Recipe Does This recipe creates an affiliate program with a fixed dollar commission per sale. Instead of earning a percentage of the order total, affiliates earn a flat $10 for every referred transaction. The payout is the same whether the customer buys a $20 item or a $300 bundle. Fixed-rate commissions work well when your product catalog spans a wide price range. A percentage model can create awkward situations where an affiliate earns $2 on a small order and $60 on a large one. A flat rate gives you predictable costs and gives affiliates a clear, simple earning structure. ## Who It's For - **Stores with varied product pricing** where percentage commissions would create unpredictable payouts for both the store and the affiliate - **Business owners who want cost certainty**, knowing exactly what each referred sale costs in commission - **Affiliate program managers** who want simple, easy-to-explain commission terms that attract a broad range of partners ## How It Works When you apply this recipe, Siren creates a program that tracks referred site visits. When someone lands on your store through an affiliate's unique link, Siren records the referral. If that visitor makes a purchase, the affiliate earns a flat $10 commission. The commission is fixed at $10 per transaction, stored internally as 1000 cents. It does not scale with order value. An affiliate referring a $25 sale and a $250 sale earns $10 in both cases. If a customer clicks links from multiple affiliates before purchasing, the most recent referral gets credit. This keeps attribution simple and predictable. Transaction compilers include line items only, so the commission triggers on product sales. Shipping, taxes, and fees do not factor into whether the commission fires, since the payout is a fixed amount rather than a percentage. > How do I pay affiliates a flat dollar amount per sale instead of a percentage? ### Program Snapshot - Best for: Stores with wide price ranges that want predictable commission costs - Main goal: Referred sales at a known fixed cost per order - Partners involved: Affiliates promoting products at any price point - Actions tracked: Referred site visits and completed purchases - Rewards supported: Fixed dollar commission per referred transaction - Starting point: Start free with Siren Lite ### What This Recipe Configures - Affiliate Program: $10.00 fixed per transaction, newest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - B2B Referral Program (https://www.sirenaffiliates.com/recipes/b2b-referral-program): The B2B Referral Program locks credit to the referrer who introduced a prospect first, so its $50 default bounty holds steady through an evaluation that drags on for months. The Fixed-Rate Affiliate Program gives the commission to the newest referral before purchase. Choose the B2B Referral Program when referrals come from clients and professional partners whose introductions take a long time to close. - Refer-a-Friend Program (https://www.sirenaffiliates.com/recipes/refer-a-friend-program): Mechanically the Refer-a-Friend Program is the same build as the Fixed-Rate Affiliate Program: a flat $10 reward on each referred sale, last-touch credit. What changes is who holds the link, since it enrolls your own customers through a registration form rather than recruited affiliates. Choose the Refer-a-Friend Program if you want word-of-mouth from existing buyers, often settled as store credit, instead of commissions to outside marketers. ### Frequently Asked Questions **What is a flat rate affiliate program?** A flat rate affiliate program pays partners a set dollar amount for every sale they refer, instead of a percentage of the order total. If your rate is $10, an affiliate earns $10 on a $25 sale and $10 on a $250 sale. The appeal is predictability: you know your cost per referred sale before it happens, and affiliates know exactly what their work is worth. **Who should use a flat rate affiliate program?** Stores whose catalogs mix inexpensive and premium products get the most out of one. A percentage commission on that kind of catalog produces tiny payouts on small orders and outsized ones on big orders, which frustrates both sides. A flat rate also fits anyone who wants commission terms they can explain to a new partner in a single sentence. **Is the commission always the same regardless of order size?** Yes. The affiliate earns the same flat amount on every referred sale, whether the order is $15 or $500. **Can I set the fixed amount to something other than $10?** Yes. Adjust the incentive amount when you apply the recipe or change it later in Siren's program settings. **What currency does this use?** The recipe defaults to USD. The fixed amount is stored in cents internally, so $10 is represented as 1000. Siren handles the conversion for display. **How does attribution work when multiple affiliates refer the same customer?** The most recent referral wins. If a customer clicks affiliate A's link on Monday and affiliate B's link on Wednesday, then buys on Thursday, affiliate B earns the commission. ## Full Sales Funnel Program Source: https://www.sirenaffiliates.com/recipes/full-sales-funnel-program A complete sales incentive system with a lead generation program, an affiliate commission program, a program group for clean attribution, and a monthly top-performer bonus distributor. Three pieces working together to cover the full funnel. ## What This Recipe Does This recipe creates a complete sales incentive system with four components: 1. **Lead Generation Program** - $15 flat bounty per qualified form submission, using first-touch attribution 2. **Affiliate Program** - 20% commission on referred sales, using last-touch attribution 3. **Sales Funnel Group** - Program group with `oldestBindingWins` sorter ensuring only one program fires per opportunity 4. **Monthly Top Performer Bonus** - Distributor that awards the full monthly revenue pool to the highest-scoring collaborator The two programs handle per-action payouts: lead bounties for form submissions and sale commissions for purchases. The program group prevents double-paying on the same prospect. The distributor adds a competitive monthly bonus on top. This is the recipe for businesses that want every stage of the funnel covered by a single incentive system. ## Who It's For - **Growing businesses** that need lead generation, affiliate commissions, and performance bonuses all in one system - **Sales managers** building a competitive incentive structure where reps earn per action and compete for a monthly prize - **Affiliate program operators** scaling beyond simple commissions into a full-funnel partner program ## How It Works The recipe has three layers, each handling a different piece of the incentive puzzle. The first layer is per-action payouts. The Lead Generation Program pays a flat $15 bounty whenever a referred visitor submits a connected form. The Affiliate Program pays a 20% commission whenever a referred visitor makes a purchase. These are the baseline incentives that compensate collaborators for each conversion they produce. The second layer is attribution control. The program group bundles both programs with an `oldestBindingWins` sorter. This means the first program to establish an engagement with a prospect claims that opportunity. If a visitor submits a form on their first interaction, the lead program gets credit. If they arrive through a referral link first, the affiliate program gets credit. This prevents a scenario where you pay both a lead bounty and a sale commission for the same person. The third layer is the monthly bonus. The distributor operates independently of the programs. At the end of each month, it evaluates all collaborators by their engagement scores and awards the entire revenue pool to the top performer. This bonus stacks on top of whatever per-action commissions a collaborator earned during the month. The distributor ships ready to run. It scores collaborators on two metrics out of the box, `referredSiteVisit` at 1 point and `boundCouponUsed` at 1 point, and the bonus pool is preset to 10% of qualifying revenue, so the monthly contest works the moment you apply the recipe. Adjust any of it in the Siren admin to fine-tune the competitive dynamics: reweight or swap the scoring events, change the pool percentage, or configure commission pool filters to control which transactions contribute revenue to the bonus pool, all without changing the underlying program structure. > Can I cover lead-gen, sales commissions, and a top-performer bonus as one coordinated system? ### Program Snapshot - Best for: Businesses rewarding lead capture, conversion, and top performance together - Main goal: Pay every funnel stage without paying twice for one prospect - Partners involved: Sales reps, affiliates, and lead-generating collaborators - Actions tracked: Form submissions, referred site visits, and completed purchases - Rewards supported: Flat lead bounties, percentage sale commissions, monthly revenue-pool bonus - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Lead Generation Program: $15.00 fixed per lead, oldest engagement wins attribution, tracked via Form submissions - Affiliate Program: 20% percentage of transaction, newest engagement wins attribution, tracked via Referral links - Program group Sales Funnel: oldest engagement wins across leadGen, affiliate - Distributor: Monthly Top Performer Bonus ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a sales funnel incentive program?** It's an incentive structure that pays partners at more than one stage of the funnel instead of only at the sale. In this recipe, that means a flat bounty when a referred prospect submits a form, a percentage commission when a referred visitor buys, and a monthly bonus for the collaborator with the highest engagement score. An attribution layer decides which program gets credit, so you only pay once per prospect. **Who should use a sales funnel incentive program?** Teams that compensate more than one kind of contribution. If some partners are good at filling the top of your funnel with leads while others close sales, a single-rate affiliate program underpays one group or the other. Splitting the funnel into a lead program and a sale program, with a shared bonus on top, lets each contribution earn what it's worth. **How do the program group and the distributor relate to each other?** They operate independently. The program group controls which program fires per opportunity (lead bounty or sale commission). The distributor is a separate monthly bonus on top of whatever commissions collaborators earned. A collaborator can earn per-action payouts from the grouped programs and also compete for the monthly bonus. **Does the monthly bonus distributor work the moment I apply the recipe?** Yes. It ships with a working default: metric tracking events of referredSiteVisit and boundCouponUsed at 1 point each, and a bonus pool preset to 10% of qualifying revenue. You can adjust all of it in the Siren admin, changing how scores are calculated (for example, swapping in collaboratorFormSubmitted), the pool percentage, or commission pool filters that control which transactions contribute to the pool. **Can a collaborator earn both the lead bounty and the monthly bonus?** Yes. The lead bounty and affiliate commission are per-action payouts governed by the program group. The monthly bonus is a separate competitive reward. A collaborator who earns lead bounties throughout the month can also win the top-performer bonus if their engagement score is the highest. **What happens if nobody generates any engagement in a given month?** If no collaborators have engagement scores when the distribution cycle runs, the pool carries over to the next month. The distributor only pays out when there is a winner. ## Instructor Revenue Share Source: https://www.sirenaffiliates.com/recipes/instructor-revenue-share A monthly revenue-sharing distributor for LMS course platforms. Instructors earn a share of subscription revenue proportional to the student engagement their courses generate. Works with LifterLMS and LearnDash. ## What This Recipe Does This recipe creates a monthly performance-weighted distributor designed for LMS course platforms like LifterLMS and LearnDash. It splits a portion of your subscription revenue among instructors based on how much student engagement their courses generate. Instructors whose courses see more lesson and course completions earn a larger share of the monthly pool. The model rewards instructors who create compelling, completable content. Rather than paying a flat rate or splitting revenue equally, this distributor ties compensation directly to student outcomes. Instructors have a clear incentive to produce courses that students actually finish. ## Who It's For - **LifterLMS and LearnDash site owners** running a multi-instructor platform who want to automate revenue sharing based on student engagement - **Online course marketplace operators** building a Udemy-style or Teachable-style experience on WordPress where instructor pay reflects course quality - **Membership site owners** who feature courses from multiple educators and need a fair, performance-based compensation model ## How It Works When you apply this recipe, Siren creates a working monthly, performance-weighted distributor. The distributor uses the `performanceSharedPool` resolver, meaning each instructor's share is proportional to their engagement score relative to all other instructors. The recipe ships with a sensible default scoring setup so it runs the moment you apply it. It scores two metrics, `courseCompleted` at 10 points and `lessonCompleted` at 1 point, a ten-to-one weighting that counts a full course completion far more than a single lesson and rewards instructors whose students see courses through to the end, and it sizes the pool at 10% of qualifying revenue. You can adjust this in your Siren admin. Instructor-driven platforms often raise the pool to 25-50% of qualifying revenue, and you can reweight the point values or set commission pool filters to include only subscription revenue, keeping one-time product sales or other income streams separate from the instructor pool. Once everything is configured, the system operates on autopilot. Siren tracks lesson and course completions throughout the month, builds each instructor's score, and distributes the accumulated pool on the first of the following month. An instructor whose courses generated 40% of all tracked completions receives 40% of the pool. > Can I share monthly subscription revenue with instructors based on how engaged their students are? ### Program Snapshot - Best for: Multi-instructor course platforms on LifterLMS or LearnDash - Main goal: Pay instructors from a monthly pool tied to student engagement - Partners involved: Course instructors bound to the courses they teach - Actions tracked: Course completions and lesson completions, scored as points - Rewards supported: A cut of the month's pooled revenue, weighted by score - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Distributor: Instructor Revenue Share ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Content Creator Profit Share (https://www.sirenaffiliates.com/recipes/content-creator-profit-share): In the Content Creator Profit Share, the metric is a reader opening a bound blog post, so traffic alone determines each author's cut of the monthly pool. The Instructor Revenue Share weights completions instead, awarding 10 points per finished course and 1 per lesson. Use the Content Creator Profit Share when your collaborators write blog posts rather than teach courses and readership is the fairest measure of their contribution. - Management Incentive Plan (https://www.sirenaffiliates.com/recipes/management-incentive-plan): Where the Instructor Revenue Share pays monthly from course and lesson completions, the Management Incentive Plan accumulates its pool for a full quarter and lets every collaborator carry a different metric, so a sales lead can be scored on coupon redemptions while a marketing lead is scored on referred visits. Go with the Management Incentive Plan when you are compensating a leadership team across mixed channels on a quarterly bonus cycle. ### Frequently Asked Questions **What is instructor revenue share?** Instructor revenue share is a compensation model where a course platform sets aside a percentage of its revenue and splits it among instructors instead of paying flat fees. In this recipe the split is weighted by student engagement, so instructors whose courses actually get finished earn a larger cut of the monthly pool. It's how marketplace platforms like Udemy pay their teachers, rebuilt on your own WordPress site. **How does instructor revenue share work?** Each month, a defined slice of subscription revenue accumulates in a shared pool while student activity gets scored. Finishing a course earns an instructor 10 points and each completed lesson earns 1, so completions carry far more weight than casual browsing. When the month rolls over, Siren divides the pool by those scores. An instructor who earned 30% of the points receives 30% of the money, and the cycle starts fresh. **Why use both courseCompleted and lessonCompleted as metrics?** Tracking both gives you a balanced scoring model. Lesson completions reward steady engagement throughout a course, while course completions reward instructors whose content students finish entirely. The recommended point values (10 for course, 1 for lesson) weight full completions more heavily. **Can I limit the revenue pool to subscription income only?** Yes. After applying the recipe, configure the commission pool filters to include only subscription-based transactions. This ensures one-time product sales or other revenue streams stay outside the instructor pool. **How are instructors connected to their courses in Siren?** Each instructor is added as a collaborator and bound to the courses they teach. When a student completes a lesson or course, Siren attributes the engagement to the bound instructor automatically. **What if an instructor publishes a new course mid-month?** The new course starts generating engagement points as soon as it is bound to the instructor. Their share of the pool adjusts naturally based on total accumulated points at distribution time. ## Instructor Team Revenue Share Source: https://www.sirenaffiliates.com/recipes/instructor-team-revenue-share A monthly revenue-share program that pays a lead instructor's teaching team automatically. Completions of the lead's courses credit each supporting instructor by seniority, and a set share of the revenue from the lead's course products funds the pool they split, with the most senior earning the largest cut. Built for LifterLMS and LearnDash course sites. ## What This Recipe Does This recipe pays a lead instructor's teaching team a share of the revenue their courses bring in, and it does it automatically every month. You set aside a slice of the revenue from the lead instructor's course products as a team pool. Each month, that pool is split among the supporting instructors on the team, with the most senior earning the biggest cut and each level down earning a little less. It is built for course businesses where one senior instructor runs a flagship program and a handful of assistant or supporting instructors help make it happen. Instead of dividing revenue equally or tracking who gets what in a spreadsheet, this program rewards your team by seniority and pays everyone on schedule. If you would rather pay every instructor purely on their own course sales with no team structure, the [Instructor Revenue Share](/recipes/instructor-revenue-share) recipe does that instead. This one is for the lead-and-team model, where a senior instructor's success is meant to lift the people who support them. ## Who It's For - **Course business owners** who run a flagship program under a lead instructor and want the supporting instructors on that team to share in its revenue - **LifterLMS and LearnDash sites** that want assistant instructors paid automatically off the lead's course revenue, without hand-tracking splits - **Education teams** using a lead-and-staff model who want the most senior instructors to earn the largest share of shared revenue ## How It Works You decide what share of the lead instructor's course revenue goes into a team pool. The default is 30 percent, and you can set it to whatever fits your business. The rest stays with you. Two separate settings shape the payout. A product category filter on the pool decides which sales count as the revenue being shared, and course completions decide how that revenue divides among the team. The recipe ships with a placeholder category, so point the filter at the category holding the lead's course products. Left unscoped, the pool would draw on every product and subscription sale in your store, not just the lead's courses. Each month, Siren builds a pool from your chosen share of the filtered revenue earned since the last payout. Every completion of the lead's courses credits the team by seniority, with the most senior supporting instructor weighted highest and each level down weighted less. At payout time the pool is divided in proportion to those accumulated credits, so seniority sets the shape of the split. With the default weighting, the top supporting instructor is weighted twice the next and four times the third. You control how steep or flat that is. You can pay up to five levels of supporting instructors below the lead, and you choose how many actually earn. This recipe pays the first three. Anyone past the levels you fund simply earns nothing on a given cycle until you decide to extend it. If a supporting instructor goes inactive, Siren skips them and they earn nothing, but nobody is promoted in their place. The rest of the team keeps their own seniority weights, and the pool divides among the points the active members earned, which is why each active share grows. When you want someone to genuinely move up, reorder the chain. The lead instructor's courses are what generate and trigger the pool, but the lead is not paid from it. This program is for rewarding the team behind the lead. If you also want to pay the lead their own share, run a second program alongside this one that pays them directly. Siren lets you run both at the same time. ## Setting Up Your Team Your teaching team is an ordered list, from the lead instructor at the top down through the supporting instructors. The order is what determines each person's share, so the closer a supporting instructor sits to the lead, the more they earn. After you apply the recipe, swap the example instructors for your own, set their order, and list the lead instructor as the educator on the courses you want feeding the program. From there you can adjust the revenue share and the seniority weighting to match how you want to pay your team. You can reorder people or add new members at any time, and the next payout reflects the change. The [managing collaborators](/documentation/getting-started/managing-collaborators-affiliates) and [configure cascade payouts](/documentation/getting-started/configure-cascade-payouts) guides cover building the team and tuning the per-level weighting. One scoping rule matters here. The cascade starts from whichever chain member is listed as a completed course's educator, and every course a chain member teaches counts. If a supporting instructor is an educator on a course of their own, its completions credit only the people below them in the chain. Keep the lead as the sole listed educator on tracked courses and the program behaves exactly as described above. Payouts run on the first of each month, and the pool is your chosen share of the revenue your category filter matched since the last payout. You configure the distributor in your Siren admin, including the schedule, the share, and the filter, so you can tune the cadence, the size, and the scope of the pool. For more background on how Siren shares revenue across a team, see [what is a cascade](/documentation/general/what-is-a-cascade), [team structures](/documentation/collaborator-group-structures/linear-chain), and the [seniority-weighted payout strategy](/documentation/calculation-strategies/downline-cascade). > How do I pay a lead instructor's teaching team a share of their course revenue, weighted by seniority? ### Program Snapshot - Best for: LifterLMS and LearnDash sites running a lead-and-team teaching model - Main goal: Pay supporting instructors a seniority-weighted share of course revenue - Partners involved: Supporting instructors ranked below the lead in a teaching chain - Actions tracked: courseCompleted events on courses the lead instructor teaches - Rewards supported: A share of the monthly revenue pool, weighted by chain layer - Starting point: Start free with Siren Lite ### What This Recipe Configures - Distributor: Teaching Team Revenue Pool ### Similar Programs, and When to Use Each - Instructor Revenue Share (https://www.sirenaffiliates.com/recipes/instructor-revenue-share): The Instructor Revenue Share has no chain at all: every instructor earns points from completions in their own bound courses, 10 for a completed course and 1 for each lesson, and the month's pool divides on those individual scores. In the Instructor Team Revenue Share, completions of the courses the lead teaches are what credit the supporting instructors arranged below that lead in the chain. Use the Instructor Revenue Share when each teacher should earn purely on engagement with their own courses, with no lead-and-team structure. - Team Performance Bonus (https://www.sirenaffiliates.com/recipes/team-performance-bonus): The Team Performance Bonus swaps the trigger that drives the Instructor Team Revenue Share: a sales lead's coupon-tracked WooCommerce orders feed its downline cascade instead of course completions, and its default pool is a leaner 10% of qualifying revenue rather than 30%. Go with the Team Performance Bonus if the chain holds sales reps on a WooCommerce store and orders through the lead's coupon should decide who shares the pool. ### Frequently Asked Questions **What is an instructor team revenue share?** An instructor team revenue share sets aside a portion of a lead instructor's course revenue and splits it among the supporting instructors who help deliver the program. Rather than equal cuts or hand-tracked stipends, the split is weighted by seniority, so the instructor closest to the lead earns the most. The payout runs on a schedule instead of living in a spreadsheet. **Who should use an instructor team revenue share?** Course businesses where one senior instructor fronts a flagship program and a small team of assistant instructors keeps it running. If you're on LifterLMS or LearnDash and currently dividing team pay by hand each month, this model automates the split while keeping senior people on the larger share. If every teacher should earn purely on their own course sales instead, a per-instructor revenue share is the better fit. **Can I change how much revenue the team shares?** Yes. You set the share that funds the team each cycle and can adjust it at any time, and the pool's category filter decides which sales count toward it. Anywhere from a quarter to half of the filtered revenue is common for teaching teams. The rest stays with you. **Which sales actually fund the team pool?** The pool draws on the product line items matched by the distributor's category filter. The recipe ships a placeholder category, so swap in the category that holds the lead's course products before going live. Without a filter, every product and subscription sale on your store would feed the pool, not just course revenue. **Why do the most senior instructors earn more?** You decide how much weight each level of seniority carries, and the shared revenue is split in proportion to those weights. With the default settings, the most senior supporting instructor earns twice what the next one does and four times the third. You control the exact split, so you can make it as flat or as steep as you want. **Does the lead instructor get paid from this?** No. This program pays the supporting instructors on the lead's team. The lead's courses are what generate and trigger the shared revenue, but the lead is not paid from this particular pool. If you want to pay the lead their own share too, run a second program alongside this one that pays the lead directly. Siren lets you run both at once. **How deep can the team go?** You can pay up to five levels of supporting instructors below the lead, and you choose how many of those levels actually earn. The recipe pays the first three by default. Add more members to extend the team, and anyone past the levels you fund simply earns nothing until you decide to fund their level. **What happens when a supporting instructor goes inactive?** They earn nothing while inactive, and nobody moves up automatically. The rest of the team keeps their own seniority weights, and the pool divides among the points the active members earned, so each active instructor's dollar share grows. To actually promote someone, reorder the chain. **Do I need LifterLMS or LearnDash for this to work?** Yes. This program is built around course completions in your LMS, so you need LifterLMS or LearnDash installed and active for Siren to record the completions that set each instructor's share. **Which instructor sits at which level?** The lead instructor sits at the top of the chain. Their courses trigger the team payout, but the lead is not paid from this pool. The first supporting instructor below the lead is level one and earns the largest share, the next is level two, and so on down the team. **What if a supporting instructor teaches courses of their own?** Completions cascade down from whichever chain member is listed as the course's educator. If a supporting instructor teaches a course of their own, its completions credit the people below them in the chain rather than the full team. For the lead-and-team model to behave as described here, make the lead the only listed educator on the courses feeding this program. **How and when does the team get paid, and what about refunds?** The pool pays out on the schedule you set, monthly by default. Each instructor can see what they have earned in the collaborator dashboard, and you pay them from Siren on your own schedule. Refunded or cancelled sales are subtracted from the revenue before the pool pays, so the team shares only what actually stuck. See the how to pay collaborators and collaborator dashboard guides, linked below. **Which Siren plan do I need?** This recipe needs Siren Pro. The seniority-weighted team payouts it relies on are a Pro feature. ## Introduce and Close Affiliate Program Source: https://www.sirenaffiliates.com/recipes/introduce-and-close-affiliate-program A dual-attribution affiliate program that pays two stacking commissions on one sale. The collaborator who first introduced the customer earns 10% on first-touch credit, the collaborator who closed the sale earns 20% on last-touch credit, and one collaborator who does both keeps all 30%. ## What This Recipe Does This recipe creates two affiliate programs that run on every sale at the same time. The introduction program pays a 10% commission to the collaborator who first brought the customer in, using first-touch attribution. The closing program pays a 20% commission to the collaborator who drove the converting click or coupon, using last-touch attribution. Because the two programs are not grouped, both can pay on a single purchase, stacking to 30% when one partner does the whole job. The point is to stop making collaborators compete for one commission. In a single-attribution program, a creator who introduced the product loses the payout the moment a deal site overwrites the cookie at checkout. Here the introduction is paid on its own track and the close is paid on another, so the educator who built awareness and the partner who converted the sale are both rewarded for the part they actually did. ## Who It's For - **Brands with both educators and closers** where a creator introduces the product through content and a deal site or affiliate drives the final click - **Affiliate managers** who want to reward the introduction without taking credit away from whoever closes the sale - **Programs that recruit content creators** who teach and review but do not hard sell, alongside performance partners who convert ## How It Works When you apply this recipe, Siren installs two programs. The introduction program uses the saleTransactionPercentage incentive with the oldestBindingWins resolver, which is first-touch attribution: the oldest referral on the customer wins. The closing program uses the same incentive with the newestBindingWins resolver, which is last-touch attribution: the newest referral or bound coupon wins. The programs are deliberately left out of any program group, because a group would force a single winner per sale. Ungrouped, each program evaluates the order on its own and pays its own commission. Attribution windows do the rest of the shaping. The introduction program carries a 60-day window, so a customer who discovers a product in a tutorial and buys seven weeks later still credits the creator who introduced them. The closing program carries a 7-day window, tuned for active promotional campaigns where the converting touch should be recent. Both windows are set on the program and can be edited in your Siren admin after install. Consider a sale with two different collaborators. Creator A published a review that earned the customer's first referred visit 40 days ago. Affiliate B sent the customer a personalized coupon that they redeemed yesterday. At checkout, Creator A holds the oldest binding and earns the 10% introduction commission. Affiliate B holds the newest binding and earns the 20% close. The store pays 30% total, split across the two partners who each did their part. Now consider a sale with one collaborator. Creator A both introduced the customer and sent the closing coupon. They hold the oldest and the newest binding, so they earn both commissions, the full 30%, on that order. No special handling is needed for solo conversions. Commissions calculate on line items only, so shipping, taxes, and fees are excluded from both rates. To tune the program, adjust the two transactionPercent values to reweight introduction against closing, or widen the introduction window when your content has a long research-to-purchase cycle. > Can I pay one collaborator for introducing a customer and another for closing the sale, on the same purchase? ### Program Snapshot - Best for: Brands that want to reward both the partner who introduces a customer and the partner who closes the sale - Main goal: Pay the introduction and the conversion without making collaborators compete for one commission - Partners involved: Content creators and educators who introduce, plus deal sites and affiliates who close - Actions tracked: First referred visit for the introduction, last referred visit or bound coupon for the close - Rewards supported: A 10% first-touch commission and a 20% last-touch commission that stack to 30% - Starting point: Start free with Siren Lite ### What This Recipe Configures - Introduction Commission: 10% percentage of transaction, oldest engagement wins attribution, tracked via Referral links - Closing Commission: 20% percentage of transaction, newest engagement wins attribution, tracked via Referral links, Coupon codes ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Multi-Touch Sales Attribution (https://www.sirenaffiliates.com/recipes/multi-touch-sales-attribution): Multi-Touch Sales Attribution divides one commission pool among every contributor to a sale, splitting a single payout by engagement so three partners share one commission. The Introduce and Close Affiliate Program does not split one payout. It pays two separate commissions at two separate rates, 10% on first-touch credit and 20% on last-touch credit, that stack on the same sale. Use Multi-Touch Sales Attribution when many partners touch a sale and you want them to share one pooled commission rather than earn a fixed introducer rate plus a fixed closer rate. - First-Touch Referral Program (https://www.sirenaffiliates.com/recipes/first-touch-referral-program): The First-Touch Referral Program pays one commission to the first affiliate who introduced the customer and pays nothing to whoever closes. The Introduce and Close Affiliate Program keeps that first-touch introduction commission and adds a second, last-touch closing commission, so the closer is paid too. Use the First-Touch Referral Program when only the original introduction should earn and the closing click should not generate a separate payout. - Full Sales Funnel Program (https://www.sirenaffiliates.com/recipes/full-sales-funnel-program): The Full Sales Funnel Program groups its first-touch lead bounty and its last-touch sale commission so only one fires per opportunity, paying a flat amount for the lead and a percentage for the sale. The Introduce and Close Affiliate Program leaves its two commissions ungrouped, so both percentages pay on the same purchase instead of one winning per opportunity. Use the Full Sales Funnel Program when an introduction is a lead worth a fixed bounty and you want each opportunity to pay exactly one collaborator per stage. ### Frequently Asked Questions **What is an introduce and close affiliate program?** It is an affiliate program that pays two commissions on one sale: one to the collaborator who first introduced the customer and one to the collaborator who closed the sale. The introduction earns on first-touch credit and the close earns on last-touch credit, so a content creator who warmed up the buyer and a deal site that converted them both get paid. **Should I pay the affiliate who introduced the customer or the one who closed?** With this recipe you pay both. Crediting only the first click rewards the collaborator who discovered the customer but leaves the closer unpaid, and crediting only the last click does the reverse. Paying a separate introduction commission and a separate closing commission means creators and educators who start the journey and the deal sites and affiliates who finish it are each rewarded for the part they did. **How do two commissions both pay on the same sale?** The recipe installs two separate programs and leaves them ungrouped. Grouping would force one program to win per sale. Without a group, the first-touch introduction program and the last-touch closing program each evaluate the order independently, so each pays its own commission. The two rates stack to a 30% maximum when one collaborator both introduced and closed. **What is the difference between first-touch and last-touch credit here?** First-touch credit pays whoever the customer met first, resolved as the oldest binding wins. Last-touch credit pays whoever the customer acted on last, resolved as the newest binding wins. The introduction program uses first-touch so the original educator is protected, and the closing program uses last-touch so the partner who drove the conversion is rewarded. **Who should use this program?** Brands that recruit content creators and educators alongside performance partners. Creators introduce a product without hard selling and deserve credit for the introduction, while deal sites and affiliates close the sale and deserve credit for the conversion. Paying both keeps neither side competing to overwrite the other's attribution. **Why is the introduction window 60 days and the closing window 7 days?** Introductions pay off slowly. Someone discovers a product in a review and buys weeks later, so the first-touch window is set to 60 days, double a typical referral window. Closing happens during active promotion, so the last-touch window is a tighter 7 days. Both windows are adjustable after install. **Can one collaborator earn both commissions?** Yes. When the same collaborator introduces the customer and also drives the closing click or coupon, they hold both the oldest and the newest binding on that sale, so they earn the 10% introduction and the 20% close for a combined 30%. **Can I change the 10% and 20% rates?** Yes. Each rate is an independent field on its own program, so you can raise the closing rate for a launch without changing the introduction rate, or the reverse. The customizable fields on this recipe expose both rates and both attribution windows before you install. **What actions does Siren track for this program?** Siren tracks referred site visits for both attribution directions and bound coupon redemptions for the close. The first referred visit opens the introduction credit, the last referred visit or a redeemed personalized coupon closes it. Commissions calculate on line items only, so shipping, taxes, and fees are excluded. **Does this work with WooCommerce and Easy Digital Downloads?** Yes. The recipe runs on WooCommerce and Easy Digital Downloads through Siren's WordPress integrations, and the same programs are reachable over the REST API for custom storefronts and headless setups. ## Invite-Only Partner Program Source: https://www.sirenaffiliates.com/recipes/reusable-partner-roster Launch a private, invite-only partner program on WooCommerce where only the partners you approve can earn. Build a curated roster of vetted partners and pay each one a percentage of every sale they drive, while the program stays closed to the public. ## What This Recipe Does This recipe gives you a private partner program where only the partners you have approved can earn. You build a roster of vetted people, and that roster is the program. Anyone on the list can earn a commission on the sales they drive. Anyone who is not on the list earns nothing, so your program stays closed to the public and entirely under your control. Each partner earns 15 percent of every qualifying sale they bring in. The rate is the same for everyone on the roster, and no one earns a cut of anyone else's sales. It is the cleanest way to reward a hand-picked group of partners without opening the door to the general public. ## Who It's For - **Agencies** running a private referral program for a small set of trusted partners instead of an open sign-up - **Brands** paying a curated group of creators while keeping the program out of public view - **SaaS companies** rewarding a short list of approved reseller or integration partners ## How It Works When you apply this recipe, Siren sets up a partner program that pays a percentage of every sale, along with a roster of the partners who are allowed to earn from it. The roster is what makes the program private. Because only the people on your roster can earn, the program is effectively closed to everyone else, even if an outsider somehow gets a partner's code. Each partner gets a unique coupon code tied to their profile, a one-time setup step per partner covered in [set up affiliate coupons](/documentation/getting-started/set-up-affiliate-coupons). When a customer enters that code at checkout, Siren [credits the partner who owns it](/documentation/general/coupon-code-used) with 15 percent of the order's product subtotal. Shipping, taxes, and fees are left out, so each partner is paid on the revenue the products produced. The code does not have to discount the order, so you can use a zero-dollar coupon purely for tracking if you would rather not change the price. As installed, the recipe tracks coupon codes only, so a tracking link will not earn anyone credit until you add link tracking to the program in your Siren admin. There is no hierarchy and no override here, so every partner earns only on the sales they personally drive. ## Managing Who Is In Your Program The roster ships with three example partners so you can see how a list is set up. These are placeholders, not real people. Before you apply the recipe, swap them out for your own approved partners using each person's real email address. You can add as many partners as you like. From there, the roster is how you run the program over time. Adding a partner gives them the ability to earn. Removing one takes it away, and that partner stops earning from that point on while the work they already drove stays on their record for you to pay out. That simple in-or-out control is the whole point of a private program: you decide exactly who is rewarded and you can change that list whenever you need to. You manage the roster and each partner's code inside Siren, covered in [managing collaborators](/documentation/getting-started/managing-collaborators-affiliates). ## Reuse the Same Roster Across Programs The roster you build here is not locked to this one program. It is a reusable list of approved partners that you can point at more than one program at once. Vet your partners a single time, then use that same group for this percentage program and for any other programs you run, so your approved partners stay eligible everywhere without you rebuilding the list each time. That reusable roster is what gives this recipe its name, and it is the reason to reach for it when you expect to run more than one offer for the same trusted group. ## When To Use This Reach for this recipe when you want a partnership that feels exclusive rather than a public affiliate sign-up. It fits agency partner networks, reseller arrangements, brand ambassador rosters you have personally vetted, and any program where letting just anyone join would dilute the relationship or the payout. If instead you want anyone to be able to apply and earn, an open affiliate program is the better starting point. This recipe requires Siren Plus for the reusable roster, since collaborator groups are a Plus feature. The closed door itself costs nothing extra, because partners on any plan earn only from programs you have assigned them to. If you later want a program where managers earn a share of their team's sales, that is a separate recipe built on a different structure. > How do I run an invite-only partner program where only a curated list of approved partners can earn commission? ### Program Snapshot - Best for: Teams running a closed program for hand-picked, vetted partners - Main goal: Reward a curated roster while the program stays closed to the public - Partners involved: Vetted partners you approve onto an invite-only roster - Actions tracked: Sales tracked through each partner's unique coupon code - Rewards supported: Percentage of the product subtotal on every coupon-tracked sale - Starting point: Start free with Siren Lite ### What This Recipe Configures - Vetted Partner Program: 15% percentage of transaction, newest engagement wins attribution, tracked via Coupon codes ### Similar Programs, and When to Use Each - Channel Partner Program (https://www.sirenaffiliates.com/recipes/channel-partner-program): The Channel Partner Program adds manual attribution beside coupon tracking and resolves conflicts with first-touch credit, so the reseller who originally landed an account keeps it. The Invite-Only Partner Program tracks coupons only and awards conflicting codes to the newest binding. Pick the Channel Partner Program when some deals close offline and long-term reseller relationships should hold their original credit. - Coupon-Based Influencer Program (https://www.sirenaffiliates.com/recipes/coupon-based-influencer-program): The Coupon-Based Influencer Program runs the same coupon-tracked 15 percent commission as the Invite-Only Partner Program but is built as a single creator campaign, with each creator carrying separate codes per platform for per-channel reporting. The Invite-Only Partner Program centers on a roster designed to be bound to several programs at once. Choose the Coupon-Based Influencer Program if the job is one creator campaign with per-platform code reporting rather than a roster you plan to reuse. - Curated Content Partner Program (https://www.sirenaffiliates.com/recipes/curated-content-partner-program): The Curated Content Partner Program swaps the tracking channel: credit comes from a reader viewing a partner's bound article before checkout, not from a coupon code at checkout, and its default rate is 20 percent where the Invite-Only Partner Program pays 15. Use the Curated Content Partner Program when partners contribute articles on your site and there is no code or link for them to share. ### Frequently Asked Questions **What is an invite-only partner program?** An invite-only partner program is a commission program that only approved partners can join and earn from. Instead of a public sign-up form, the operator curates a roster of vetted partners, and being on that list is what makes someone eligible to earn. It's the structure to reach for when the partnership itself is selective and an open application would cheapen it. **Who should use an invite-only partner program?** Anyone whose partnerships depend on trust rather than volume. Agencies with a short list of referral partners, brands working with a curated set of creators, and SaaS companies with approved resellers all fit the model. If you'd rather let anyone apply and start earning, an open affiliate program serves you better. **How do I keep my partner program private so only approved partners can earn?** That is exactly what this recipe does. You build a roster of the partners you have approved, and only the people on that roster can earn from the program. Anyone who is not on your list earns nothing, even if they get hold of a code. That is what makes it a true invite-only program instead of an open sign-up. **Can I change the commission rate after installing?** Yes. The recipe pays 15 percent by default, and you can set any rate you want before you apply it or adjust it later in your Siren admin. Every partner on the roster earns the same rate. **Can I add or remove partners later?** Yes. The roster ships with three placeholder partners so you can see how a list is structured. Replace them with your own approved partners, using each partner's real email address. Adding someone to the roster gives them the ability to earn, and removing someone takes it away, so you control who is in your program at any time. **Does this program pay anyone a cut of other partners' sales?** No. Every partner earns only on the sales they personally drive. There are no overrides and no payouts to anyone above them. If you want a structure where managers earn a share of their team's sales, that is a different recipe. **How does Siren know which partner drove a sale?** Give each partner on your roster a unique coupon code. When a customer uses that code at checkout, Siren credits the partner who owns it with their commission. As installed, the program watches coupon redemptions and nothing else. If some partners would rather share a tracking link, add link tracking to the program in your Siren admin first. **Does the coupon code have to give the customer a discount?** No. You can create a coupon with a zero-dollar discount and use it purely for tracking, so the code identifies the partner without changing the order total. If you do want the code to offer a discount, that works too, and the commission is still calculated the same way. **What counts as a qualifying sale?** The commission is a percentage of the product subtotal on the order, the value of the items themselves. Shipping, taxes, and fees are left out, so each partner is paid on the revenue the products actually produced. You can adjust which parts of an order count in your Siren admin. **Can I pay different partners different commission rates?** Not within this single program. Every partner on the roster earns the same percentage. If you need different rates for different partners, run more than one program, each with its own rate, and place each partner in the program that fits their deal. **Can I reuse the same roster for more than one program?** Yes. The roster is a reusable list of approved partners, and you can bind it to more than one program at once. Build your vetted roster a single time, then point it at this percentage program and any other programs you run, so the same approved group stays eligible across all of them without rebuilding the list each time. **How and when do partners get paid, and what about refunds?** Siren tracks what each partner has earned, and you pay them on your own schedule. Partners can see their earnings in the collaborator dashboard. Because credit follows real sales, a refunded or cancelled order is handled by Siren's standard payout rules. See the how to pay collaborators and collaborator dashboard guides, linked below. **Which Siren plan do I need for this?** This recipe requires Siren Plus, and the reason is the reusable roster, not the privacy. A program is private on its own at every plan level, since a partner earns only after you assign them to it directly. What Plus adds is the collaborator group, one vetted list you can bind to several programs and edit in a single place. **Do I need WooCommerce for this to work?** Yes. Siren calculates commissions from your store transactions, so WooCommerce, Easy Digital Downloads, or North Commerce must be installed and active. ## Management Incentive Plan Source: https://www.sirenaffiliates.com/recipes/management-incentive-plan A quarterly performance-weighted distributor for management and leadership teams. Distributes a share of revenue based on each manager's area of responsibility and the results their teams produce. ## What This Recipe Does This recipe creates a quarterly performance-weighted distributor designed for management and leadership incentive compensation. It pools a percentage of qualifying revenue over a three-month period and distributes it proportionally based on each manager's engagement score. Managers whose areas of responsibility generate stronger results receive a larger share. The quarterly cadence is deliberate. It gives leaders enough time to execute strategy, absorbs short-term fluctuations, and aligns payouts with the business review cycles most organizations already follow. ## Who It's For - **Business owners** who want to incentivize department heads and team leads with performance-based quarterly bonuses tied to real revenue outcomes - **Operations managers** looking for a structured, automated way to calculate and distribute management bonuses without spreadsheets - **Growing companies** that need to align leadership compensation with measurable business metrics across marketing, sales, and content teams ## How It Works When you apply this recipe, Siren creates a working quarterly, performance-weighted distributor. The distributor uses the `performanceSharedPool` resolver, dividing the accumulated pool proportionally based on each manager's engagement score over the quarter. The recipe ships with a sensible default scoring setup so the plan runs the moment you apply it. It scores managers on two metrics, `referredSiteVisit` at 1 point to reflect inbound traffic performance and `boundCouponUsed` at 1 point to reflect deal conversions, and it sizes the pool at 10% of qualifying revenue. This is where you tailor scoring to each manager's area of responsibility, so adjust it in your Siren admin. Add `boundPostUsed` for a content manager, reweight the point values to reflect the relative importance of each metric, change the pool percentage, or set commission pool filters to specify which transaction types count toward the pool. The quarterly cadence aligns with how most businesses already operate. Siren tracks engagement throughout the quarter and distributes on the first day of the following quarter. This structure gives managers a clear line of sight between their team's performance and their compensation, while keeping the administrative overhead near zero. > Is there a way to run a quarterly performance-weighted bonus pool for my management team? ### Program Snapshot - Best for: Companies tying leadership bonuses to quarterly business results - Main goal: Pay managers a revenue share weighted by team performance - Partners involved: Department heads, team leads, and executives added as collaborators - Actions tracked: Referred site visits, coupon redemptions, and post engagement by role - Rewards supported: Performance-weighted share of a quarterly revenue pool - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Distributor: Management Incentive Plan ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Content Creator Profit Share (https://www.sirenaffiliates.com/recipes/content-creator-profit-share): The Content Creator Profit Share narrows the pooled split to a single metric and a faster clock: every writer earns points from visits to the posts bound to them, and the pool settles as each new month begins. The Management Incentive Plan instead tracks each manager on role-specific events and closes out once a quarter. The Content Creator Profit Share fits better if the people being paid are blog writers and monthly readership is the only score that matters. - Instructor Revenue Share (https://www.sirenaffiliates.com/recipes/instructor-revenue-share): The Instructor Revenue Share is the monthly LMS version of the pooled payout: points come from courseCompleted and lessonCompleted events at a ten-to-one weighting, while the Management Incentive Plan scores each leader on whatever engagement events their department generates and distributes quarterly. The Instructor Revenue Share is the better fit if monthly subscription revenue gets divided among LifterLMS or LearnDash instructors. ### Frequently Asked Questions **What is a management incentive plan?** A management incentive plan ties part of a leadership team's compensation to measurable business results. Instead of fixed bonuses, managers earn a share of a defined pool, in this case a percentage of quarterly revenue, weighted by the performance of the areas they're responsible for. It's the leadership equivalent of a sales commission: pay that moves with outcomes. **How does a management incentive plan work?** Over the quarter, a set percentage of qualifying revenue builds into a shared pool while each manager racks up an engagement score from the events their area owns. When the quarter closes, the pool splits in proportion to those scores. If the sales lead's coupon redemptions outpaced everything else, they take home the biggest cut, and a manager whose area lagged earns a smaller one. **Why quarterly instead of monthly?** Quarterly distribution aligns with standard business review cycles. It gives managers a full quarter to execute strategy and smooths out month-to-month variance, producing a more meaningful measure of sustained performance. **Can different managers be tracked on different metrics?** Yes. Each manager is a collaborator bound to the engagement types relevant to their role. A marketing manager might be tracked on referredSiteVisit, while a sales manager is tracked on boundCouponUsed. Siren scores each based on the events tied to them. **What if a manager joins mid-quarter?** They begin accumulating points from the moment they are added as a collaborator. Their share at distribution time reflects the engagement generated from that point forward. **Can I exclude certain revenue types from the pool?** Yes. The commission pool filters let you include or exclude specific transaction types. You can limit the pool to product revenue, subscription revenue, or any combination that fits your business. ## Marketplace Vendor Commission Source: https://www.sirenaffiliates.com/recipes/marketplace-vendor-commission The revenue-share core of a multi-vendor marketplace. Every vendor whose product appears in a transaction earns their percentage independently, the platform keeps the rest. Built as the commission engine for marketplace MVPs and established multi-vendor stores. ## What This Recipe Does This recipe is the revenue-share core of a multi-vendor marketplace. It isn't a commission program bolted onto a store, it's the engine that splits every transaction between the vendor who supplied the product and the platform that runs the storefront. Each vendor earns 70% of the revenue from their own product sales, and the marketplace platform keeps the remaining 30%. When a customer places an order containing products from multiple vendors, every vendor whose products appear in that order earns their commission independently. The key difference from a standard affiliate program is the resolver: this recipe uses "every binding wins" instead of "newest binding wins." In a typical affiliate setup, only one collaborator earns per transaction. In a marketplace, that would mean only one vendor gets paid per order, which breaks the entire model. With "every binding wins," all vendors with products in the order earn simultaneously. ## Starting a Marketplace with One Recipe Applying this recipe is the smallest viable shape of a *working* multi-vendor marketplace. It's the minimum a marketplace needs to run: vendors as collaborators, products linked to them, and a revenue split that fires on every sale. Day one looks like this. You apply the recipe, add your vendors as collaborators, link each vendor to the products they're selling, and start taking orders. The commission math runs itself from there. None of the heavier pieces have to exist for the marketplace to start running, and an operator can validate the idea against real revenue splits before deciding whether to build vendor self-service screens, a custom storefront, or escrow logic. A marketplace has a storefront, a payments rail, and a commission engine sitting between them. Siren is the *commission engine*. The storefront is what WC Vendors, Dokan, or custom WordPress code handle, including vendor dashboards, product editing, and the shop-by-vendor pages. Payments are a separate problem. The operator's payment processor takes the customer's card. Siren's job is the layer in between, tracking which vendor earned what, on which sale, under which program. The full picture is laid out in [the marketplaces guide](/documentation/getting-started/marketplaces). The mechanic doesn't change when the vertical does. Vendors get a percentage, the platform keeps the rest, and "every binding wins" makes multi-vendor orders settle cleanly. A course marketplace runs on it ([Online Course Platform Starter](/recipes/online-course-platform-starter) shows that flavor with instructors and students). A vacation rental marketplace runs on it too ([Travel Destination Marketplace](/recipes/travel-destination-marketplace) shows the host-and-listing flavor). Handmade goods, B2B parts catalogs, services marketplaces, they all fit the same shape. This recipe is what every one of those verticals has in common underneath. ## Who It's For - **Founders building a marketplace MVP** who need vendor revenue-sharing working before they invest in vendor onboarding UX or a custom storefront, shipping a working marketplace fast instead of building commission plumbing from scratch - **Multi-vendor marketplace operators** running storefronts where each vendor lists and sells their own products through a shared platform - **WooCommerce store owners** using a multi-vendor or marketplace plugin who want a clean, trackable commission system for paying vendors their share ## How It Works When you apply this recipe, Siren creates a program that watches for a specific engagement event: collaborator product sold. Each vendor in your marketplace is added as a collaborator and linked to their products. When a customer purchases a product, Siren checks which vendor owns it and records the engagement. The "every binding wins" resolver is what makes this work for marketplaces. Unlike resolvers that pick a single winner per transaction, this one pays every collaborator who has a qualifying engagement. If a customer's cart contains items from three different vendors, all three earn their 70% cut on their respective sales. No vendor is excluded because another vendor's product was also in the cart. The 70/30 split is a common marketplace default. The vendor keeps the majority because they supply the product, handle fulfillment, and bear inventory risk. The platform keeps 30% for providing the storefront, customer acquisition, and infrastructure. You can adjust this split when applying the recipe or change it later in the program settings. Commissions are calculated on line item totals only. Shipping, taxes, and fees are excluded, giving you a clean cost structure that vendors can understand and predict. The recipe *tracks* what each vendor earns. It doesn't move money on their behalf. Every sale adds to the accrued balance for the vendor who owned the product, and that balance is visible in the collaborator dashboard and exposed through the REST API. Siren doesn't initiate vendor payouts. Operators run those payouts on their own schedule, through whatever rail fits the business. Once a payout is sent, mark it paid in Siren and the accrual resets cleanly. The mechanics are walked through in [the guide on paying collaborators](/documentation/getting-started/how-to-pay-collaborators). > How do I build a multi-vendor marketplace where every vendor earns their share on every sale? ### Program Snapshot - Best for: Marketplace founders and multi-vendor WooCommerce operators - Main goal: Split every sale between the vendor and the platform - Partners involved: Vendors selling their own products through your storefront - Actions tracked: Sales of vendor-owned products in WooCommerce orders - Rewards supported: Percentage of each vendor's own product sales - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Vendor Commission Program: 70% percentage of transaction, every engagement wins attribution, tracked via Owned product sales ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Course Creator Royalty Program (https://www.sirenaffiliates.com/recipes/course-creator-royalty-program): The Course Creator Royalty Program credits course sales through the newest binding wins resolver and defaults instructors to a 50% share, an even split where the Marketplace Vendor Commission runs a vendor-majority 70/30 arrangement. Choose it for a multi-instructor education site where each course's creator should earn half of every enrollment. - Product Royalty Program (https://www.sirenaffiliates.com/recipes/product-royalty-program): The Product Royalty Program tilts the split toward the platform: creators take 40% of their line items by default while the store keeps the rest, and attribution resolves with newest binding wins where the Marketplace Vendor Commission relies on every binding wins. It's the better fit when you operate the store yourself and pay artists or designers a minority royalty on the work they contribute. ### Frequently Asked Questions **What is a marketplace vendor commission?** A marketplace vendor commission is the share of each sale a vendor earns when their product sells on a platform they don't own. The percentage runs in the opposite direction from an affiliate rate: the vendor takes the majority, commonly around 70%, because they supply the product and handle fulfillment, while the platform keeps the rest for running the storefront. **How does a marketplace vendor commission work?** Each product on the marketplace belongs to one vendor, and Siren reads that ownership on every incoming WooCommerce order, crediting each owner 70% of their line items. Orders containing several vendors settle independently, so every vendor gets paid on their own products. **What happens when a customer buys products from multiple vendors in one order?** Every vendor whose product appears in the order earns their commission independently. If a customer buys from three vendors, all three receive 70% of their respective product sales. There is no competition between vendors. **How does Siren know which products belong to which vendor?** Each vendor is added as a collaborator and linked to their products. When a customer purchases a linked product, Siren automatically attributes the sale to the correct vendor. **Can I set different commission rates for different vendors?** This recipe applies a single rate to all vendors in the program. To set per-vendor rates, you would create separate programs for each vendor or adjust individual collaborator settings in the Siren admin. **Does the 70% include shipping and taxes?** No. Commissions are calculated on line item totals only. Shipping, taxes, and fees are excluded from the calculation. **Is this enough to run a marketplace, or do I need other plugins?** This recipe runs the commission math. It doesn't give you a vendor-facing storefront, an application form, or a dashboard where vendors edit their own products. Most operators pair Siren with a marketplace plugin like WC Vendors or Dokan for the storefront layer, or roll custom WordPress code when they want full control over the experience. The [marketplaces guide](/documentation/getting-started/marketplaces) walks through the full stack. **How do I pay vendors their earned commission?** Siren tracks accrued commission per vendor in the collaborator dashboard and through the REST API. Operators run payouts on their own schedule through whatever channel fits the business, bank transfer, the operator's payment processor, or manual check. Mark payouts as paid in Siren once they're sent, and the accrued balance resets. The [guide on paying collaborators](/documentation/getting-started/how-to-pay-collaborators) covers the workflow. ## Milestone Rewards Program Source: https://www.sirenaffiliates.com/recipes/milestone-rewards-program A monthly competitive distributor that awards the entire revenue pool to the collaborator with the highest engagement score. Designed for milestone-based incentives across any collaborator type. ## What This Recipe Does This recipe creates a monthly competitive distributor that rewards the single highest-scoring collaborator with the entire revenue pool. It is built around the concept of hitting milestones and targets. The collaborator who accumulates the most engagement points by the end of the month wins everything. Unlike a flat sales bonus, this distributor is designed to work across any type of collaborator and any combination of engagement metrics. Whether you are running a program for affiliates, content creators, course instructors, or a mix of all three, you define what "winning" means by choosing which engagement types to track and how to weight them. ## Who It's For - **Program managers** who want a flexible, competitive reward structure that works across different collaborator types, not just sales teams - **Course platform operators** looking to incentivize the instructor who drives the most student engagement each month - **Content-driven businesses** that want to reward the creator who reaches the highest performance milestones within a monthly cycle ## How It Works When you apply this recipe, Siren creates a working monthly, winner-take-all distributor. The distributor uses the `topScoreWins` resolver, which awards the entire accumulated pool to the collaborator with the highest engagement score at the end of each cycle. The recipe ships with a sensible default scoring setup so the competition runs the moment you apply it. It awards 1 point per `referredSiteVisit`, 5 points per `boundCouponUsed`, and 10 points per `courseCompleted`, a composite that rewards completions and coupon-closed orders more heavily than raw traffic, and it sizes the pool at 10% of qualifying revenue. This is where you define what "winning" means, so adjust it in your Siren admin to fit your program. Swap in `boundPostUsed` for a content creator program, add `lessonCompleted` for a course platform, reweight the point values, change the pool percentage, or set commission pool filters to determine which transactions contribute. Each month resets cleanly. Siren tallies all engagement scores, identifies the single highest scorer, and distributes the full pool to that person on the first of the following month. The flexibility of this structure means you can reshape the competition at any time by adjusting which metrics are tracked and how they are weighted. > How can I award a monthly revenue pool to whichever affiliate hit the highest engagement score? ### Program Snapshot - Best for: Programs rewarding measurable engagement targets across mixed collaborator types - Main goal: Push collaborators toward monthly engagement milestones - Partners involved: Affiliates, content creators, course instructors, mixed rosters - Actions tracked: Site visits, coupon redemptions, post usage, course and lesson completions - Rewards supported: Winner-take-all monthly revenue pool - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Distributor: Milestone Rewards Program ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Monthly Sales Bonus (https://www.sirenaffiliates.com/recipes/monthly-sales-bonus): The Monthly Sales Bonus runs the same winner-take-all revenue pool as the Milestone Rewards Program but scores the contest on just two signals, referral traffic and coupon-tracked checkouts. The Milestone Rewards Program opens the scoreboard to any tracked engagement, including post usage and course or lesson completions, weighted however you choose. Pick the Monthly Sales Bonus when the contest is strictly affiliates and sales reps competing on traffic and coupon-closed orders. ### Frequently Asked Questions **What is a milestone rewards program?** A milestone rewards program pays out based on defined performance targets, like a set number of sales, course completions, or engagement points, rather than a flat commission per transaction. In this recipe, those targets feed a monthly competition: every tracked action adds to a collaborator's score, and the highest score at the end of the cycle wins the full revenue pool. **Who should use a milestone rewards program?** Anyone running a program where engagement is measurable and worth competing over. Course platforms use it to reward whichever instructor drives the most student activity, and content stores point it at their most productive creators. Affiliate teams treat it as a monthly performance prize, and mixed rosters work too, since every collaborator type scores on the same configurable metrics. **How is this different from the Monthly Sales Bonus recipe?** The Monthly Sales Bonus is framed for affiliate and sales teams competing on referral traffic and coupon usage. The Milestone Rewards Program is broader. It works for any collaborator type and any combination of engagement metrics, making it suitable for content creators, instructors, affiliates, and mixed programs. **Can I track multiple engagement types at once?** Yes. You can configure several metric tracking events with different point values. For example, you might award 1 point per site visit, 5 points per coupon redemption, and 10 points per course completion. The total score across all configured metrics determines the winner. **What happens during months with low activity?** The pool still distributes to whoever has the highest score, even if overall activity is low. If no collaborator has any engagement at all, there is nothing to distribute. You can pause the distributor during slow periods if needed. **Can I change the metrics being tracked between months?** Yes. You can update metric tracking events in your Siren admin at any time. Changes take effect immediately and apply to the current cycle going forward. **Does this require a specific Siren plan?** Yes. This recipe is built on a distributor, which schedules and pools rewards over time. Distributors require the Siren Essentials tier. ## Monthly Sales Bonus Source: https://www.sirenaffiliates.com/recipes/monthly-sales-bonus A competitive monthly bonus distributor where the top-performing affiliate or sales representative wins the entire revenue pool. One winner takes all. ## What This Recipe Does This recipe creates a competitive monthly bonus distributor where a single winner takes the entire pool. At the end of each month, the collaborator with the highest engagement score receives 100% of the accumulated revenue pool. Everyone else receives nothing from this distributor for that cycle. This is a winner-take-all structure designed to drive competition. It works well as a supplemental incentive layered on top of standard commission programs, giving your top performer an extra reward for outpacing the field. ## Who It's For - **Affiliate program managers** who want to add a competitive bonus layer that rewards their single best-performing affiliate each month - **Sales team leaders** looking for a monthly contest structure that motivates representatives to push harder for the top spot - **Store owners** who want to create urgency and friendly competition among their partners without replacing existing commission structures ## How It Works When you apply this recipe, Siren creates a working monthly, winner-take-all distributor. The distributor uses the `topScoreWins` resolver, which awards the entire pool to the collaborator with the highest engagement score at the end of the cycle. The recipe ships with a sensible default scoring setup, so it runs the moment you apply it. It scores collaborators on two metrics, `referredSiteVisit` at 1 point per referred visit and `boundCouponUsed` at 1 point per coupon redemption, and it sizes the pool at 10% of qualifying revenue. You can adjust all of this in your Siren admin. Drop one of the metrics if you run a pure affiliate or pure sales-rep team, reweight the point values, change the pool percentage, or set commission pool filters to determine which transactions contribute to the pool. The monthly cycle resets on the first of each month. Siren tallies scores, identifies the single highest scorer, and pays out the full pool to that person. Scores reset, and the competition begins again. This creates a recurring incentive that keeps collaborators engaged month after month. > Can I run a winner-takes-all monthly sales bonus for my top-performing rep or affiliate? ### Program Snapshot - Best for: Stores and sales teams running a monthly top-performer contest - Main goal: Drive competition with one winner-take-all prize each month - Partners involved: Affiliates and sales reps competing for the monthly pool - Actions tracked: Referred site visits and bound coupon redemptions - Rewards supported: A revenue-share prize pool the monthly winner claims outright - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Distributor: Monthly Sales Bonus ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Milestone Rewards Program (https://www.sirenaffiliates.com/recipes/milestone-rewards-program): The Milestone Rewards Program and the Monthly Sales Bonus each pay the entire pool to the single top scorer on the first of the month. The Milestone Rewards Program widens what counts as a point, scoring post usage and course and lesson completions alongside site visits, where the Monthly Sales Bonus keeps score on referral traffic and coupon redemptions alone. Choose the Milestone Rewards Program when instructors, content creators, or a mixed roster should compete on engagement beyond sales activity. ### Frequently Asked Questions **What is a monthly sales bonus program?** A monthly sales bonus program is a recurring incentive that pays out on a fixed calendar cycle based on performance. This recipe runs the winner-take-all version: a percentage of the month's revenue accumulates into a pool, and the affiliate or sales rep with the highest engagement score claims all of it. **How does a monthly sales bonus program work?** Each cycle follows the calendar. Partners spend the month building engagement scores from tracked events like referred site visits or coupon redemptions while the revenue pool grows alongside them. When the calendar flips, Siren hands the entire pool to whoever finished with the highest score, wipes every score back to zero, and a fresh round begins. **What happens if two collaborators tie for the highest score?** Siren uses a tiebreaker to determine the winner. The collaborator who reached the top score first receives the full pool. **Can I use this alongside a standard commission program?** Yes. Distributors and programs operate independently. You can run a per-sale commission program for baseline payouts and layer this bonus on top as an extra incentive for the highest performer each month. **What metrics should I track for sales reps versus affiliates?** For affiliates, referredSiteVisit tracks how many visitors they send to your store. For sales reps who use discount codes, boundCouponUsed tracks coupon redemptions. You can configure one or both depending on your team structure. **How large should the revenue pool percentage be?** That depends on how motivating you want the bonus to be. Even a small percentage of monthly revenue can create a meaningful prize. Start with a percentage you are comfortable with and adjust based on how your team responds. ## Multi-Touch Sales Attribution Source: https://www.sirenaffiliates.com/recipes/multi-touch-sales-attribution Split affiliate commissions fairly across every collaborator who helped close a sale. When one affiliate drives traffic through a referral link and another closes with a coupon code, both earn an equal share of the commission. ## What This Recipe Does This recipe creates a single affiliate program with multi-touch attribution. Instead of awarding the entire commission to one affiliate, it splits the payout equally among every collaborator who engaged with the customer before the sale. If one affiliate drove traffic through a referral link and another closed the deal with a coupon code, both earn an equal share of the 20% commission. This is the right choice when your affiliates often work in complementary roles and you want every contributor rewarded fairly. ## Who It's For - **Store owners with overlapping affiliate channels** where content creators drive awareness and coupon partners close sales - **Partnership marketers** who pair bloggers, influencers, and deal sites in coordinated campaigns - **Anyone who finds winner-take-all attribution unfair** and wants a commission model that reflects the full customer journey ## How It Works When you apply this recipe, Siren creates a program that tracks two engagement types: referred site visits and bound coupon usage. As a customer moves through your store, Siren records every affiliate interaction along the way. One affiliate might send the customer to your site through a referral link. Later, that same customer might use a coupon code tied to a different affiliate. When the customer completes a purchase, Siren looks at all the collaborators who engaged with that customer and divides the commission equally among them. If two affiliates contributed, each gets 10% of the transaction. If three contributed, each gets roughly 6.7%. This "evenly shared pool" approach eliminates disputes about who deserves credit. Every affiliate who played a role in the conversion gets a fair share. The commission is calculated on line items only, so shipping, taxes, and fees are excluded. > What's the best way to split a commission evenly across every affiliate who contributed to closing the sale? ### Program Snapshot - Best for: Stores whose affiliates overlap on the same customers - Main goal: Share commission credit across the full customer journey - Partners involved: Content creators, influencers, and coupon-based closers - Actions tracked: Referred site visits, bound coupon usage, completed sales - Rewards supported: Percentage commission split equally among contributors - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Multi-Touch Attribution Program: 20% percentage of transaction, shared equally attribution, tracked via Referral links, Coupon codes ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Split Commission Program (https://www.sirenaffiliates.com/recipes/split-commission-program): The Split Commission Program divides one 20% commission by weight rather than evenly. Contributor shares scale with engagement score, so 300 points collects triple the cut that 100 points does. Pick the Split Commission Program when equal shares would underpay the affiliates who clearly did most of the work. - Top Performer Affiliate Program (https://www.sirenaffiliates.com/recipes/top-performer-affiliate-program): The Top Performer Affiliate Program names a single winner per sale. Siren compares cumulative engagement scores and hands the full 25% commission to the highest scorer, leaving every other contributor unpaid on that transaction. Choose the Top Performer Affiliate Program when you want affiliates competing head to head instead of sharing credit. ### Frequently Asked Questions **What is multi-touch sales attribution?** Multi-touch sales attribution gives credit to every partner who interacted with a customer before a purchase, instead of handing the whole commission to a single winner. In this recipe, any affiliate who sent a referred site visit or had their bound coupon used counts as a contributor. When the sale completes, the commission divides equally among all of them. **Who should use multi-touch sales attribution?** Stores where affiliates routinely overlap on the same customer. If a blogger drives the first visit and a coupon site closes the deal, a single-touch model pays one of them and leaves the other with nothing. An even split keeps both partners motivated to keep doing what they each do best. **How does the commission actually split?** Siren divides the commission equally among every collaborator who engaged with the customer before the sale. If three affiliates contributed, each receives one-third of the total commission. **What counts as a qualifying engagement?** This recipe tracks two engagement types: referred site visits (link clicks) and bound coupon usage. Any collaborator who triggered either event for the customer is included in the split. **What happens if only one affiliate engaged with the customer?** That affiliate receives the full commission. The even split only applies when multiple collaborators contributed to the same conversion. **Can I add more engagement types later?** Yes. You can edit the program in your Siren admin to add additional engagement types like product sales or form submissions. Any new engagement type will factor into the attribution pool. ## New Business vs Renewal Commission Source: https://www.sirenaffiliates.com/recipes/new-business-vs-renewal-commission A two-rate commission plan that pays one rate when a customer first buys and a lower rate when an existing customer renews. A program group reads the order type and fires exactly one rate per transaction. ## What This Recipe Does This recipe creates two commission programs bundled in a program group: 1. **New Business** - 15% commission on a customer's first order 2. **Renewal** - 7% commission when an existing customer renews Both programs credit the same reps and calculate commission the same way. The difference is the rate and the order type each one responds to. The program group reads whether an order is a new sale or a renewal and fires exactly one of the two, so a rep never earns both rates on a single transaction. Use this recipe when landing a new account is worth more to you than keeping one, and you want that split enforced automatically instead of reconciled by hand every pay period. ## Who It's For - **Sales teams** that pay a premium for landing a new account and a maintenance rate to keep it - **Subscription and contract businesses** where renewals should earn less than first-time sales - **Sales managers** who want the new-versus-renewal split enforced automatically instead of sorted out by hand ## How It Works Each rep is credited on the accounts they own. When one of those accounts places an order, Siren looks at the order type. A first-time sale routes to the New Business program and pays 15%. A renewal of an existing account routes to the Renewal program and pays 7%. The program group is what keeps the two rates from colliding. Both programs are bundled together, which tells Siren they are two halves of one plan rather than independent programs. A given order is either new business or a renewal, never both, so the group fires exactly one rate per transaction. The most recently triggered engagement takes priority if anything overlaps. You are not limited to two rates. After applying this recipe you can add a third program to the group, for example a win-back rate for a lapsed account that returns, or a separate rate for an upsell on an existing account. The group handles the mutual exclusivity regardless of how many rates you add. Commissions are calculated on line items, so shipping, taxes, and fees are excluded from the payout. > How do I pay reps a higher rate on new business than on renewals? ### Program Snapshot - Best for: Sales teams that reward landing a new account more than keeping one - Main goal: Pay a premium on new business and a maintenance rate on renewals - Partners involved: Your own reps, account executives, or partners - Actions tracked: New sales and renewals, separated by order type - Rewards supported: Two percentage rates, one for new business and one for renewals - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - New Business: 15% percentage of transaction, newest engagement wins attribution, tracked via Coupon codes - Renewal: 7% percentage of transaction, newest engagement wins attribution, tracked via Coupon codes - Program group New Business and Renewal: newest engagement wins across newBusiness, renewal ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Tiered Affiliate Program (https://www.sirenaffiliates.com/recipes/tiered-affiliate-program): The Tiered Affiliate Program switches rates based on a partner's standing, Standard versus VIP, not on the order type. Both of its tiers fire on the same kind of sale. Choose the Tiered Affiliate Program when the higher rate should reward a promoted top performer, not a new-business order. ### Frequently Asked Questions **What is a new business vs renewal commission plan?** It is a commission plan that pays one rate when a customer buys for the first time and a different, usually lower, rate when an existing customer renews. The premium on new business rewards reps for the harder work of landing an account, while the renewal rate keeps paying them to retain it. **How does Siren know whether an order is new business or a renewal?** Each program responds to a specific order type. The New Business program fires on a new sale and the Renewal program on a renewal, and Siren takes which an order is from your platform's data. On a subscription platform that is the first purchase versus a recurring renewal. On an order-based system, including a radio traffic and billing system, the new-versus-renewal classification is established at setup from your order data and your definition rather than a built-in rule, since it is rarely a clean flag. A classification can be corrected before payout. **What stops a renewal from paying the new-business rate?** The program group. Both programs sit in one group, and the group fires exactly one of them per order based on the order type. Without the group, a rep enrolled in both could earn twice on a single transaction. **Can I add a third rate, like a win-back rate?** Yes. Add another program to the group with its own rate and order type. The group handles mutual exclusivity for any number of rates, so a lapsed-account win-back rate can sit alongside new business and renewal. **Does this require a specific Siren plan?** Yes. Program groups require the Essentials tier. Renewal tracking also requires a platform that reports renewals, such as a subscriptions extension or a billing system that emits renewal events. ## Online Course Platform Starter Source: https://www.sirenaffiliates.com/recipes/online-course-platform-starter The complete LMS monetization package. Affiliates earn 30% for referring students, instructors earn 50% royalty on their courses, and a monthly revenue share distributes subscription income to instructors based on student engagement. ## What This Recipe Does This recipe creates a complete course marketplace monetization system with three components: 1. **Course Affiliate Program** - 30% commission to affiliates who refer students, tracked via referral link visits 2. **Instructor Royalty Program** - 50% royalty to instructors when their courses sell, tracked via product ownership bindings 3. **Instructor Revenue Share** - Monthly distributor that splits subscription revenue among instructors based on student engagement with their courses The two programs are deliberately not grouped. When a student buys a course, the affiliate who referred them earns 30% and the instructor who created the course earns 50%. Both fire on the same transaction because they reward different people for different roles. The distributor adds a recurring revenue layer. Each month, it pools a percentage of subscription income and distributes it to instructors proportionally based on how much student engagement their courses generated. Instructors whose courses see more completions earn a larger share. This is the recipe that turns a WordPress site with LifterLMS or LearnDash into a full course marketplace. ## Building a Udemy-Style Platform on WordPress If you've ever wanted to run your own version of Udemy, this recipe is the Siren half of that stack. Pair it with LifterLMS or LearnDash for course delivery and WooCommerce for payments, and you've got the core of a multi-instructor education marketplace: instructors publish and sell their own courses, affiliates bring in new students, and the platform earns a cut of every transaction. The three programs bundled here handle the compensation math so you don't have to track sales by hand or calculate instructor royalties in a spreadsheet. The video above walks through a full buildout of this pattern using LifterLMS, but the same approach works with LearnDash. It covers LMS setup, Siren installation, and the three programs this recipe creates in one step. It's a useful reference if you want to see how the pieces fit together in a real WordPress site. ## Who It's For - **LifterLMS and LearnDash site owners** building a multi-instructor marketplace where affiliates acquire students and instructors create content - **Education entrepreneurs** launching a Udemy-style or Teachable-style platform on WordPress - **Platform operators** who want automated compensation for both content creators and promoters without manual payouts ## How It Works The recipe has three pieces that operate simultaneously, each handling a different part of the marketplace economy. The Course Affiliate Program handles student acquisition. Affiliates share referral links to drive traffic to the platform. When a referred visitor purchases a course, the affiliate earns 30% of the transaction. Attribution uses `newestBindingWins`, so the most recent referral determines who gets credit. This is the growth engine of the marketplace. The Instructor Royalty Program handles creator compensation on direct sales. When an instructor is bound to a course in Siren and that course sells, the instructor earns 50% of the line item total. This happens automatically based on product ownership, with no referral link needed. The instructor simply needs to be associated with their courses in Siren. On a single sale, both the affiliate and the instructor earn because the programs are independent. The Instructor Revenue Share handles subscription income. Many course platforms sell access through monthly or annual memberships rather than individual course purchases. The distributor pools a percentage of that subscription revenue each month and distributes it to instructors based on student engagement. The `performanceSharedPool` resolver means each instructor's share is proportional to their engagement score relative to all other instructors. The distributor ships ready to run. It comes with two metric tracking events, `courseCompleted` at 10 points and `lessonCompleted` at 1 point, a model that weights full course completions heavily and rewards instructors who create courses that students finish, and the pool is preset to 10% of qualifying revenue. The system runs on autopilot from the moment you apply it. Siren tracks completions throughout the month, builds each instructor's score, and distributes the accumulated pool on the first of the following month. Adjust the pool percentage or reweight the metrics in the Siren admin, and configure the commission pool filters to include only subscription revenue if you want to keep one-time course purchases out of the pool. The combined economics are straightforward. Direct course sales pay the affiliate (30%) and the instructor (50%), leaving 20% as platform revenue. Subscription revenue pays instructors monthly through the performance pool. You control the pool percentage, so your margin on subscription income is whatever you choose. > Can I set up affiliates, instructor royalties, and a monthly revenue share for my course platform in one go? ### Program Snapshot - Best for: LifterLMS and LearnDash sites running multi-instructor marketplaces - Main goal: Pay promoters and instructors automatically from every revenue stream - Partners involved: Affiliates who refer students and instructors who create courses - Actions tracked: Referral link visits, course sales, course and lesson completions - Rewards supported: Percentage commissions, instructor royalties, performance-weighted pool shares - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Course Affiliate Program: 30% percentage of transaction, newest engagement wins attribution, tracked via Referral links - Instructor Royalty Program: 50% percentage of transaction, newest engagement wins attribution, tracked via Owned product sales - Distributor: Instructor Revenue Share ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is an online course marketplace?** An online course marketplace is a platform where multiple instructors publish and sell their own courses, the way Udemy or Skillshare do. The platform handles delivery and payments, keeps a cut of each sale, and compensates instructors for the rest. Affiliates often sit alongside that, earning commissions for the students they bring in. **How does an online course marketplace work?** Instructors create courses, students buy them or subscribe, and the platform splits the revenue among everyone involved. With this recipe, a direct sale pays 30% to the referring affiliate, 50% to the instructor, and leaves 20% for the platform. Subscription income is pooled monthly and shared among instructors based on how much of their content students actually complete. **Why are the affiliate and royalty programs not in a group?** Because they compensate different people for different contributions. The affiliate drove the student to the platform. The instructor created the course. Both deserve to be paid from the same sale, so the programs stack intentionally. **Does the distributor work the moment I apply the recipe?** Yes. The distributor ships with a working default scoring setup: metric tracking events of courseCompleted at 10 points and lessonCompleted at 1 point, which weights full course completions ten times more than individual lesson completions, plus a monthly pool preset to 10% of qualifying revenue. You can adjust any of it in the Siren admin, reweighting the metrics, changing the pool percentage, or configuring commission pool filters to include only subscription revenue and keep one-time course purchases separate. **Can an instructor also be an affiliate?** Yes. A collaborator can be enrolled in both programs. They would earn royalties on sales of their own courses and affiliate commissions for referring students to other instructors' courses. The programs evaluate independently. **What if I do not sell subscriptions?** The distributor works with any revenue stream. If you sell courses individually rather than through memberships, configure the commission pool filters to include those transactions instead. The performance-weighted distribution model works regardless of how students pay. ## Pay-Per-Lead Affiliate Program Source: https://www.sirenaffiliates.com/recipes/pay-per-lead-affiliate-program A lead generation affiliate program where affiliates earn a flat fee for every qualified form submission they refer. First-touch attribution ensures the affiliate who originally introduced the lead gets credit. ## What This Recipe Does This recipe creates a single lead generation program where affiliates earn a flat $25 bounty for every qualified form submission they refer. Instead of tracking sales, it tracks form submissions as the conversion event. The first affiliate to introduce a lead to your site gets credit for that lead, regardless of later interactions. This is the right program structure for businesses where the primary goal is capturing leads, not closing immediate sales. ## Who It's For - **Service businesses** that convert customers through consultations, demos, or quote request forms - **SaaS companies** that want affiliates driving trial signups or free registrations - **Lead-dependent businesses** where form submissions are worth a predictable dollar amount and sales happen offline or later in the funnel ## How It Works When you apply this recipe, Siren creates a program that listens for a single engagement event: a collaborator form submission. This event fires when a visitor who was referred by an affiliate submits a form connected to Siren through a compatible form plugin like Gravity Forms. Attribution uses first-touch logic. The first affiliate to engage with a prospect through any tracked interaction is the one who earns the bounty when that prospect submits a form. If Affiliate A sent the visitor to your site originally and Affiliate B later interacted with the same person, Affiliate A still gets credit. This "oldest binding wins" approach rewards affiliates for discovering new leads rather than retargeting warm prospects. Each qualifying form submission triggers a flat $25 commission. The amount does not depend on any transaction total because there is no sale involved. This gives you a predictable, fixed cost per lead that is easy to budget around. > How do I pay affiliates a flat fee for every qualified lead they send through a form? ### Program Snapshot - Best for: Service and SaaS businesses where sales close long after the form - Main goal: Reward each qualified prospect at one flat rate - Partners involved: Affiliates who source prospects rather than close sales - Actions tracked: Qualified form submissions from referred visitors - Rewards supported: Flat $25 bounty per lead, no percentage math - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Lead Generation Program: $25.00 fixed per lead, oldest engagement wins attribution, tracked via Form submissions ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Cost-Per-Lead Campaign (https://www.sirenaffiliates.com/recipes/cost-per-lead-campaign): The Cost-Per-Lead Campaign pays whichever affiliate touched the prospect most recently before the form submission, a last-touch rule, while the Pay-Per-Lead program credits the original introducer. The Cost-Per-Lead bounty also defaults to $15 per qualifying lead rather than $25. Choose the Cost-Per-Lead Campaign for short, time-boxed pushes where leads convert within days and the affiliate working the prospect last has earned the payout. ### Frequently Asked Questions **What is a pay per lead affiliate program?** A pay per lead affiliate program flips the usual commission model: partners earn a set dollar amount for each qualified prospect they bring in, not a cut of whatever sale eventually closes. In this recipe, a lead is a form submission from a referred visitor, worth a flat $25 to the affiliate who introduced that prospect first. It fits businesses where deals close through consultations or demos, long after the affiliate's part is done. **Who should use a pay per lead affiliate program?** Businesses with long sales cycles or high-ticket offers, where expecting an affiliate to deliver a closed sale isn't realistic. Your affiliates are good at finding prospects, not negotiating contracts, so you pay them at the point where they actually have influence: the form fill. Service firms booking consultations and SaaS companies rewarding trial signups are the typical cases. **Which forms count as a qualifying lead?** Any form connected to Siren through a compatible form plugin. Gravity Forms is the most common integration. You configure which forms trigger the collaboratorFormSubmitted event in your Siren settings. **Why does the first affiliate get credit instead of the last?** Lead generation rewards the affiliate who originally introduced the prospect. The first affiliate found the lead, so they earn the bounty. This is first-touch attribution and it encourages affiliates to find new audiences rather than retarget existing ones. **Can I change the bounty amount after installing?** Yes. Edit the program's incentive amount in your Siren admin. The new amount applies to all future leads. Past commissions are not affected. **Does this work without WooCommerce?** Yes. This program tracks form submissions, not sales. You need a compatible form plugin like Gravity Forms, but WooCommerce is not required. ## Product Royalty Program Source: https://www.sirenaffiliates.com/recipes/product-royalty-program A general-purpose royalty program for product creators, vendors, artists, or designers. Creators earn a percentage of every sale of their assigned products, tracked automatically through product ownership. ## What This Recipe Does This recipe creates a royalty program where product creators earn a percentage every time one of their products sells. Artists, designers, vendors, and other creators are assigned ownership of specific products in your WooCommerce store. When a customer buys one of those products, the creator automatically earns a 40% royalty on the sale. This is the same ownership-based tracking model used by the Course Creator Royalty recipe, but framed for general commerce. Think print-on-demand artists, marketplace vendors, product designers, or any scenario where a creator should earn revenue from the products they contribute to your store. ## Who It's For - **Marketplace operators** who list products from multiple vendors and need automated royalty payments per sale - **Print-on-demand store owners** compensating artists and designers when their work sells - **Multi-vendor WooCommerce shops** looking for a transparent, automated revenue-sharing model that scales with their catalog ## How It Works When you apply this recipe, Siren creates a program that watches for one engagement event: a collaborator's product being sold. You assign each product to its creator using Siren's owned products feature. Once that relationship is set, tracking is automatic. A customer buys a product, Siren identifies the creator who owns it, and credits them with a 40% royalty. There are no referral links or coupon codes involved. Attribution is based entirely on product ownership. This makes the system simple for creators, who do not need to promote or share links. They create products, you list them, and they earn when those products sell. If a customer places an order containing products from multiple creators, each creator earns independently on their own products. A cart with items from three different designers generates three separate royalty credits. The royalty is calculated on each product's line item value, so each creator's payout reflects the actual price of their work. The 40% default rate works well for marketplaces and print-on-demand stores where the platform handles hosting, payments, and fulfillment while creators supply the product itself. Adjust the rate up or down to match your margin structure and the value your creators provide. > How do I pay product creators a royalty whenever their own products sell? ### Program Snapshot - Best for: Marketplaces, print-on-demand stores, and multi-vendor WooCommerce shops - Main goal: Share revenue automatically with the creators behind each product - Partners involved: Artists, designers, vendors, and other product creators - Actions tracked: Sales of creator-owned products, recorded per line item - Rewards supported: Percentage royalty on each sale of an owned product - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Product Royalties: 40% percentage of transaction, newest engagement wins attribution, tracked via Owned product sales ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Course Creator Royalty Program (https://www.sirenaffiliates.com/recipes/course-creator-royalty-program): Mechanically the Course Creator Royalty Program matches the Product Royalty Program, ownership-based credit on every sale of an assigned product with the newest binding wins resolver. What changes is the catalog and the default: it assigns courses to instructors and starts the royalty at 50% rather than the Product Royalty Program's 40%. Use it when you run an LMS site and want instructors earning half of each course sale out of the box. - Marketplace Vendor Commission (https://www.sirenaffiliates.com/recipes/marketplace-vendor-commission): The Marketplace Vendor Commission program inverts the Product Royalty Program's math, handing vendors a 70% cut of what they sell, and it resolves with every binding wins, letting a cart that spans several vendors settle every commission in a single pass. Choose it when independent vendors stock and fulfill the catalog and your platform's cut is the smaller side of the split. ### Frequently Asked Questions **What is a product royalty program?** A product royalty program pays creators a percentage every time a product they made sells. The reward attaches to the product itself rather than to referral activity, so the creator earns because they own the work, not because they drove the traffic. Print-on-demand stores and multi-vendor marketplaces use this model to share revenue with the artists and vendors behind their catalogs. **How does a product royalty program work?** You assign each product to the creator who made it, and the program watches for sales of those products. When one sells, the creator is credited a set percentage of that product's line item value, 40% by default in this recipe. There are no links to share or codes to track, so creators earn whether the sale came from your marketing or theirs. **How do I assign products to a creator?** Add the creator as a collaborator in Siren, then use the owned products feature to link their products to their profile. Sales of those products are tracked automatically from that point on. **Can one product belong to multiple creators?** Yes. Assign the product to each creator. When it sells, every creator who owns it earns the royalty independently. **Does this work for digital downloads and physical products?** Yes. Any WooCommerce product type works, including simple products, variable products, and downloadable products. If WooCommerce can sell it, Siren can track royalties on it. **What Siren plan do I need?** This recipe requires the Essentials tier. Product ownership tracking and the collaboratorProductSold engagement type are Essentials features. ## Refer-a-Friend Program Source: https://www.sirenaffiliates.com/recipes/refer-a-friend-program A customer referral program that rewards existing customers with a flat $10 credit for every friend they refer who makes a purchase. Automated referral program software built into WordPress. ## What This Recipe Does This recipe creates a referral program designed for your existing customers. Each customer gets a unique referral link. When they share it with a friend and that friend makes a purchase, the referrer earns a flat $10 reward in store credit. Unlike a traditional affiliate program, this is framed as a customer loyalty feature. The language, structure, and reward model are designed around everyday customers sharing with people they know, not professional marketers running campaigns. Customers think in dollar amounts, not percentages, so the reward is a simple flat rate. ## Who It's For - **Store owners who want organic growth** through customer word-of-mouth, with a structured incentive to encourage sharing - **Brands building community** that want to reward loyal customers for bringing in new buyers - **Subscription and membership businesses** looking for a low-cost, automated way to acquire new customers through existing ones ## How It Works When you apply this recipe, Siren creates a referral program that your customers can join. You set up a registration form (Siren provides tools for this) where customers sign up and receive a unique referral link. When a customer shares their link and a friend clicks it, Siren records a referred site visit and binds that friend to the referrer. If the friend goes on to make a purchase, the referrer earns a $10 reward. The reward is a fixed amount of store credit, stored internally as 1000 units and valued one to one with your store currency, so 1000 means $10.00 of credit. It does not change based on what the friend buys. Whether the friend spends $15 or $150, the referrer earns $10 of store credit. If a friend clicks referral links from two different customers before purchasing, the most recent referral gets credit. This keeps the program straightforward for customers who do not need to think about attribution rules. The referral reward is paid in store credit, a custom currency the referrer redeems on a future order. You review and approve each earned balance, then issue it by hand on the free tier or let a credit autofulfiller post it automatically as a checkout coupon (a Siren Essentials feature). Either way the credit stays inside your store and pulls the referrer back for another purchase. Transaction compilers include line items only, so the reward triggers on actual product purchases. Shipping, taxes, and fees are excluded from the trigger conditions. ## Automating store credit at checkout The referrer is rewarded in a custom store-credit currency rather than cash, so the reward stays inside your store and brings them back for another order. Defining that currency and rewarding referrers in it are free-tier features, and on the free tier you issue each balance by hand. To make the reward automatic, the way the program is framed, add a credit autofulfiller. It turns each earned balance into a coupon that applies at the referrer's next checkout, with no manual step. The binding is a Siren Essentials feature, so the program runs free and the automation is the upgrade. Add it by binding the store-credit currency to the built-in `credit` fulfillment method: ```json { "autofulfillers": { "storeCreditAuto": { "currency": "STORE_CREDIT", "method": "credit" } } } ``` > How do I build a refer-a-friend program that automatically rewards customers with store credit? ### Program Snapshot - Best for: Ecommerce and subscription stores growing through customer word-of-mouth - Main goal: Turn happy customers into a steady source of new buyers - Partners involved: Existing customers who share a personal referral link - Actions tracked: Referred site visits and friend purchases, newest referral credited - Rewards supported: Flat $10 of store credit per referred purchase - Starting point: Start free with Siren Lite ### What This Recipe Configures - Referral Program: $10.00 fixed per transaction, newest engagement wins attribution, tracked via Referral links ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Automatic credit fulfillment (Siren Essentials): Issue the store credit or points a customer earns automatically, as a coupon that applies at their next checkout, instead of granting it by hand. - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - B2B Referral Program (https://www.sirenaffiliates.com/recipes/b2b-referral-program): The B2B Referral Program pays a larger default bounty, $50 per closed deal, and resolves credit first-touch, meaning the original introducer keeps the reward even when other referral links get clicked during a months-long decision. The Refer-a-Friend Program credits the most recent link. Pick the B2B Referral Program when your referrers are clients or business contacts introducing high-value deals with long sales cycles. - Fixed-Rate Affiliate Program (https://www.sirenaffiliates.com/recipes/fixed-rate-affiliate-program): The Fixed-Rate Affiliate Program follows the same rulebook as the Refer-a-Friend Program, paying $10 flat per sale to whoever referred the buyer last, but its partners are outside affiliates promoting your store rather than customers passing a link to friends. The Fixed-Rate Affiliate Program is the better fit when you are recruiting marketers and content creators instead of rewarding your own customer base. ### Frequently Asked Questions **What is a refer-a-friend program?** A refer-a-friend program rewards your existing customers for bringing in people they know. Each customer gets a personal link, and when a friend uses it to make a purchase, the customer earns a reward. It's a loyalty feature aimed at everyday buyers rather than a commission scheme for professional marketers. **How does a refer-a-friend program work?** A customer signs up through a registration form and receives a unique referral link. When a friend clicks that link, Siren records the visit and ties the friend to the referrer. If the friend goes on to buy, the referrer earns a flat $10, which you can pay out as store credit or through any method you prefer. **How does a customer get their referral link?** Set up a program registration form in Siren. Customers sign up through the form and receive a unique referral link they can share with friends. **Can I offer a discount to the referred friend as well?** This recipe handles the referrer's reward only. To offer the friend a discount, create a WooCommerce coupon and share it alongside the referral link in your registration confirmation messaging. **Is the $10 reward given as store credit or cash?** Store credit. The reward is paid in a custom store-credit currency the referrer spends on a future order, which keeps the reward inside your store. You can issue each balance by hand on the free tier, or add a credit autofulfiller (a Siren Essentials feature) to turn it into a coupon that applies automatically at checkout. **What stops people from referring themselves with fake accounts?** Siren tracks referrals by engagement events, so basic self-referral is possible with separate accounts. For most stores, the flat reward amount keeps abuse low. If fraud is a concern, review pending commissions before approving payouts. ## Rep Split and Manager Override Source: https://www.sirenaffiliates.com/recipes/rep-split-and-manager-override A sales commission plan where co-selling reps split a deal's commission and the sales manager earns a monthly override on total team revenue. One program shares the per-deal commission; one distributor pays the manager's override. ## What This Recipe Does This recipe creates two pieces that work together: 1. **Rep Commission** - a 10% commission on each deal, shared among the reps who closed it 2. **Sales Manager Override** - a distributor that pays the sales manager 5% of total team revenue each month The reps split the deal between themselves when they co-sell, and the manager earns a separate override on everything the team bills. The two payouts are independent: the override is funded from its own revenue pool, not deducted from the reps' commission. Use this recipe when your comp plan has both an individual rep payout and a leadership override, and you want both calculated from the same sales instead of maintained in separate spreadsheets. ## Who It's For - **Sales teams** where two reps often co-sell one account and should share the commission - **Sales managers** who earn an override on the revenue their team produces - **Comp plans** that combine an individual rep payout with a leadership override on top ## How It Works The Rep Commission program uses an evenly shared pool. When a deal closes, the program divides its commission among the reps credited on that deal. Two reps sharing an account each take half; a single rep on a deal takes the whole commission. This handles the common case where accounts are co-owned without anyone reconciling splits by hand. The Sales Manager Override is a distributor, not a program. It accumulates a percentage of qualifying team revenue into a pool over the month, and on the first of the next month it pays that pool to the manager bound to it. Because the override is a share of total revenue rather than of any one deal, it rewards the manager for the team's whole result. Enroll more than one manager and the pool splits evenly between them. The two pieces read the same sales but never compete: the program pays the reps on each deal, and the distributor funds the override from a separate revenue share. Adjust the per-deal commission rate, the override percentage, and the payout schedule in the Siren admin after applying the recipe. Rep commissions are calculated on line items, so shipping, taxes, and fees are excluded from the payout. > Can two reps split a deal while the sales manager earns an override on the whole team? ### Program Snapshot - Best for: Manager-led sales teams where reps co-sell and leadership earns an override - Main goal: Split deals fairly between reps and pay the manager on team revenue - Partners involved: Reps who share accounts, plus the sales manager over them - Actions tracked: Sales credited to reps, pooled monthly for the override - Rewards supported: A shared per-deal commission and a percentage override on team revenue - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Rep Commission: 10% percentage of transaction, shared equally attribution, tracked via Coupon codes - Distributor: Sales Manager Override ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Split Commission Program (https://www.sirenaffiliates.com/recipes/split-commission-program): The Split Commission Program shares a deal between contributing reps but has no leadership override on top. It is the per-deal half of this plan without the manager distributor. Choose the Split Commission Program when reps share deals but no manager earns an override on team revenue. - Monthly Sales Bonus (https://www.sirenaffiliates.com/recipes/monthly-sales-bonus): The Monthly Sales Bonus pays its monthly pool to the single highest-scoring rep as a contest, not to a manager as an override on everyone's revenue. Choose the Monthly Sales Bonus when the monthly pool should reward a top performer rather than pay a manager an override. - Sales Override Commission Program (https://www.sirenaffiliates.com/recipes/tiered-sales-override-program): Rep Split and Manager Override funds the manager override from a monthly revenue pool shared by the team, so the override is a percentage of total team billing rather than a credit tied to each rep individual sale. The Tiered Sales Override Program pays the override along a ranked reporting line per deal, crediting each manager above the rep the moment that rep closes. Choose the Tiered Sales Override Program when the override should fire per deal up a defined reporting line on Siren Pro, rather than from a monthly team pool on Essentials. ### Frequently Asked Questions **What is a manager override?** A manager override is a commission a sales manager earns on the revenue their team produces, on top of what the individual reps earn. It rewards leadership for the team's total result rather than for personally closing deals. In this recipe the override is a percentage of total team revenue, paid monthly. **How do two reps split a single deal?** Both reps are credited on the shared account, and the Rep Commission program divides the deal's commission between them. When only one rep is on a deal, that rep earns the full commission, so solo deals need no special handling. **The recipe splits evenly. How do I do a weighted 60/40 split?** The recipe ships an even split as the starting point, using a shared pool that divides a deal equally among the reps on it. For weighted splits, the same program switches to a performance-weighted pool so each rep's share follows the agreed split rather than an even one. You set the weighting per account or per deal during setup, and each rep sees their own percentage. **Is the manager override on gross or net, and on billing or collections?** You set the base explicitly. The override pools a percentage of team revenue, and you choose whether that is gross billing or net, billed or collected, and whether agency business is included. If you pool on billing, a later non-payment reduces the pool so the override trues up. If you pool on collections, it only counts money already received. **Does the manager override reduce what the reps earn?** No. The reps earn their shared commission on the deal, and the manager's override is funded separately from a percentage of team revenue. They are two independent payouts, not one pool the manager takes a cut of. **When does the manager override pay out?** On a monthly cycle. The override distributor pools a set percentage of qualifying team revenue through the month and pays the manager on the first of the next month, then resets. You can adjust the percentage and the schedule in the Siren admin. **Can I have more than one sales manager on the override?** Yes. Enroll each manager on the override distributor and the monthly pool divides evenly between them. This fits a team with co-managers or a regional structure where several leaders share the override. **Does this require a specific Siren plan?** Yes. Distributors require the Essentials tier, since the manager override is a scheduled distributor that pools team revenue over time. ## Sales Override Commission Program Source: https://www.sirenaffiliates.com/recipes/tiered-sales-override-program Launch a sales commission plan that pays each rep a percentage on the deals they close and splits a fixed override pool among their managers on those same sales. Set how many levels of management share the pool and how much each level earns. Built for internal sales teams on WooCommerce. ## What This Recipe Does This recipe gives your sales team a commission plan where managers get paid when their people sell. Every rep earns a commission on the deals they close, and the managers above each rep earn an override on those same sales, so leaders share in the results of the team they lead and coach. Each rep is credited through their own coupon code. When a customer checks out using a rep's code, Siren credits that sale to the rep and runs the override up the reporting line above them. The rep earns a percentage of the order, and a separate, fixed override pool is split among their managers. You decide how far up the line the override reaches and how that pool is split. The rep keeps their full commission either way, because their pay and the override are kept completely separate. The override is an extra cost your business chooses to pay, and it never comes out of the rep's commission. Best of all, you do not have to wire any of this up by hand. Applying the recipe builds the whole plan in one step: the rep commission, the management override, your sales team roster, and the link between them. It ships with a few example reps so you can see the structure right away. Swap them for your real team and the plan is live. ## Who It's For - **Sales teams** that want managers to earn an override on every deal their reps close - **Business owners** who want reps and their managers paid automatically on the same sale, with no spreadsheets to reconcile - **Sales leaders** who need management overrides across several levels without building a separate plan for each one - **Manufacturers and wholesalers with dealer networks** that put regional managers over dealer reps and owe overrides on sell-through, wherever the deal actually closes ## How It Works Two things happen on every sale closed with a rep's coupon, and they are kept apart so no one is double-counted. First, the rep who closed the deal earns a straight commission on their own sale, calculated as a percentage of the order. This is their personal pay, and it is the same whether or not they have a manager above them. Second, the managers above that rep share a fixed override pool on the same sale. The pool is a set dollar amount you choose, for example $50, and it does not grow with the order total or with the number of managers above the rep. You give each level a weight, and Siren splits the pool among the active managers in proportion to those weights, up to five levels above the rep. With the default weights, the rep's direct manager earns the largest share and each level higher up earns less. For example, a $50 pool split 100, 50, and 25 across three managers pays about $28.57 to the direct manager, $14.29 to the next, and $7.14 to the third, and the shares always add up to $50. You can change the weights to match how your business rewards leadership, and you can stop the override at any level by setting that level's weight to zero. Because the pool is a fixed amount, your override cost per sale is predictable no matter how deep your team goes. If a manager in the line is inactive, they are passed over and earn nothing on that sale. The managers who are active still split the same pool between them, so none of it is lost and your cost per sale stays the same. If a rep has no active manager above them yet, no override is paid on that sale. ## Setting Up Your Team The plan is driven by your sales roster, which orders your people from the top of the reporting line down to the front line. The recipe ships with example reps so you can see the structure right away. Replace them with your own team, give each person their place in the line and their own coupon code, and adjust the rep commission and override pool to fit your compensation plan. One thing to know about the shipped roster: it installs as a [linear chain](/documentation/collaborator-group-structures/linear-chain), a single line of command where each person has exactly one person above them and one below. That fits one rep and the managers over them, but a manager with several reps needs a branching shape, because in a single line senior reps would sit in their peers' upline and collect the override on their colleagues' deals. If that describes your team, apply the recipe, then open the group in your Siren admin, switch its structure to the [parent-child tree](/documentation/collaborator-group-structures/parent-child), and point each rep at their manager. The pool, the per-layer weights, and the relay all behave exactly the same on the tree, and the override now climbs from each rep straight through their own managers. Not every deal your team closes ends at a web checkout. When a sale happens over the phone, in the field, or through a dealer, enter or import it as an order and attribute it to the closing rep, either from the Transactions screen in your Siren admin or via the REST API. Enable the [manual attribution](/documentation/general/manual-attribution) trigger on both programs, and the commission and the override pay out exactly as if the rep's code had been used at checkout, so Siren's commission tracking covers field and dealer sales alongside your online orders. As your team changes, you update the roster, not the plan. Add new hires, move people when they are promoted, and the overrides keep flowing to the right managers automatically. For a closer look at how the reporting line and overrides work, see [what is a cascade](/documentation/general/what-is-a-cascade). To give your reps their tracking codes, see [set up affiliate coupons](/documentation/getting-started/set-up-affiliate-coupons). ## What You Can Build Without Pro Most of this plan runs below Pro. The roster itself, a collaborator group, is a Siren Plus feature. What needs Siren Pro is ranking that roster into a reporting line and relaying per-deal credit up it. The rest of the machinery here, programs, percentage commissions, and [program groups](/documentation/getting-started/multiple-affiliate-programs-using-sirens-program-groups), runs on Siren Essentials. That matters because two simpler compensation shapes get most teams what they need without a cascade. If senior people should simply earn a higher rate on their own deals, one program per tier does exactly that on any plan, including Lite, the same shape the [Tiered Affiliate Program](/recipes/tiered-affiliate-program) recipe uses, and Essentials adds the program group that keeps two tiers from paying on the same deal. And if managers should be paid from the team's overall results rather than credited deal by deal, an Essentials revenue pool covers it, which is what the [Management Incentive Plan](/recipes/management-incentive-plan) recipe sets up. Reach for this recipe when the override has to follow individual sales up a reporting line automatically. > How do I pay my sales managers an override on the deals their reps close, on top of each rep's own commission? ### Program Snapshot - Best for: Internal sales teams on WooCommerce with managers over reps - Main goal: Pay managers an override on the deals their reps close - Partners involved: Closing reps plus up to five levels of managers above them - Actions tracked: Sales closed through each rep's personal coupon code - Rewards supported: Percentage commission for the rep, a weighted override pool for managers - Starting point: Start free with Siren Lite ### What This Recipe Configures - Sales Base Commission: 10% percentage of transaction, newest engagement wins attribution, tracked via Coupon codes - Sales Override Cascade: $50.00 fixed per transaction, performance weighted attribution, tracked via Coupon codes ### Similar Programs, and When to Use Each - Sales Team Commission Program (https://www.sirenaffiliates.com/recipes/sales-team-commission-program): The Sales Team Commission Program keeps the coupon tracking but drops the chain entirely: every coupon-tracked sale pays the rep a flat $10, and nothing reaches their manager. The Sales Override Commission Program adds the ranked reporting line and the $50 pool that pays it. Use the Sales Team Commission Program when reps should be paid on their own closes and no manager needs a cut of the deal. - Team Performance Bonus (https://www.sirenaffiliates.com/recipes/team-performance-bonus): Credit runs the opposite direction in the Team Performance Bonus: the team lead's coupon-tracked sales score the reps below them, and a monthly pool funded by 10% of qualifying revenue splits on those scores. The Sales Override Commission Program instead pays the managers above each closing rep, deal by deal. Reach for the Team Performance Bonus when the reward should flow down from a selling lead to their team monthly rather than up to management per deal. - Tiered Affiliate Program with Overrides (https://www.sirenaffiliates.com/recipes/tiered-affiliate-cascade): The Tiered Affiliate Program with Overrides aims the Sales Override Commission Program's upline relay at outside partners: affiliates promote through referral links rather than coupon codes, the referring affiliate takes a flat $20 rather than a percentage, and the pool that climbs the chain is $10 per sale, not $50. Go with the Tiered Affiliate Program with Overrides when the chain is made of outside affiliates recruiting each other, not employees you manage. ### Frequently Asked Questions **What is a sales override commission?** A sales override commission is pay a manager earns on deals closed by the reps under them. The rep keeps their own commission on the sale, and the override is paid on top of it to the people up the reporting line. It gives managers a direct financial stake in coaching their team to close. **Who should use a sales override commission plan?** Any sales org where managers are accountable for their reps' numbers: inside sales teams, agencies with team leads, brokerages with producing managers. If you're reconciling overrides in a spreadsheet today, this replaces the spreadsheet. It matters less for flat teams, since an override only pays once at least one manager sits above the closing rep. **How does Siren know which rep closed a sale?** Each rep gets their own coupon code. When a customer checks out using that code, Siren credits the sale to that rep and runs the override up the line above them. Reps can share their code over the phone, by email, or anywhere they sell, so this works for an inside sales team, not just link-sharing affiliates. See the coupon setup guide, linked below. **My reps' sales happen in the field, not on my website. Can this still track them?** Yes, once you enable the manual attribution trigger on the program. Enter the sale as an order, or import it, then attribute it to the closing rep from your Siren admin or via the REST API, and the override reaches the managers above that rep just as a coupon-tracked sale would. Earnings from those deals land in the same real-time commission reporting as everything else. See the manual attribution guide, linked below. **Is the override a percentage of the sale or a fixed amount?** It is a fixed pool per sale, not a percentage, so it does not change with the order total. The default is a $50 pool on each qualifying sale, and you can set that to whatever you want. The rep's own commission is the percentage, 10% of the order by default. **Does the rep who closes the deal earn the override too?** No. The rep who closes the sale earns their own commission, set by the base rate. The override is a separate pool paid only to the managers above that rep. It is an extra cost your business chooses to pay, and it never comes out of the rep's commission, so nobody is paid twice on the same role. **How many levels of management can earn an override?** Up to five levels above the closing rep. You decide how deep it goes. If you only want the rep's direct manager to earn an override, fund just the first level and leave the rest at zero. **Can one manager have several reps reporting to them?** Yes, with one adjustment after install. The recipe ships the roster as a single ordered line, which models exactly one person per level. If a manager oversees several reps, open the group in your Siren admin, change its structure to the parent-child tree, and set each rep's manager. The override pool and the per-layer weights work exactly the same way on the tree. **Can I pay each level of management a different amount?** Yes. You give each level a weight, and the override pool on each sale is split among the managers in proportion to those weights. The default gives the closest manager the biggest share and smaller shares to each level above, but you control the split. **What happens if a manager's seat is empty?** An inactive manager is passed over and earns nothing on that sale. Because the override is a fixed pool split by weight, the managers who are active simply split the whole pool between them, so none of it is lost and your total override cost per sale stays the same. **What happens on a refund or cancellation?** If an order is refunded or cancelled, Siren reverses the commission for that sale, including the override paid up the line, so a sale that did not stick does not stay paid. **How and when does everyone get paid?** Siren tracks what the rep and each manager earned, and you pay them on your own schedule. Everyone can see their earnings in the collaborator dashboard. See the how to pay collaborators and collaborator dashboard guides, linked below. **Can I change the rates after I install it?** Yes. You can adjust the rep's base commission, the size of the override pool on each sale, and how it splits between levels at any time from your Siren admin. **Can I run this for affiliates instead of an internal sales team?** Yes. The same override structure works for partner and affiliate programs. There is a partner-facing version of this same idea, the Tiered Affiliate Program with Overrides recipe, linked from the section above. **What plan and platform do I need?** This recipe runs on Siren Pro and works with WooCommerce, where Siren calculates commissions from your orders. Pro is what ranks your roster into a reporting line and relays per-deal credit up it, while the roster itself is a Plus feature. If senior reps just need a higher rate on their own deals, one program per rate does that on any plan, including Lite, and Essentials adds the program group that keeps two tiers from paying on the same deal. If managers should be paid from overall team revenue, an Essentials revenue pool covers it. The What You Can Build Without Pro section above walks through those options. **Do I have to set up each program separately?** No. Applying this recipe builds the whole plan at once: the rep commission, the management override, your sales team roster, and the connection between them. The only thing left to do is swap the example reps for your real team and give each one their coupon code. ## Sales Team Commission Program Source: https://www.sirenaffiliates.com/recipes/sales-team-commission-program An internal commission program for your sales team. Each rep gets a unique coupon code, and when a customer uses it at checkout, that rep earns a flat $10 commission. Built for employees, not external affiliates. ## What This Recipe Does This recipe creates a single commission program for your internal sales team. Each sales rep receives a unique coupon code. When a customer uses that code at checkout, the rep who owns it earns a flat $10.00 commission on the sale. This is not an affiliate program for external marketers. It is an employee incentive system. The framing, tracking, and payout logic are all designed for staff members you manage directly, whether they work a retail floor, a call center, or a service desk. ## Who It's For - **Retail store owners** who want to reward floor associates for driving sales with personal promo codes - **Car dealerships and service businesses** tracking per-rep performance through unique employee codes - **Call center and service desk managers** incentivizing reps to close sales during customer interactions ## How It Works When you apply this recipe, Siren creates a program that listens for one engagement event: a bound coupon being used at checkout. You assign each sales rep a unique WooCommerce coupon code linked to their collaborator profile. The code can offer a customer discount or not. Either way, when a customer enters the code during checkout, Siren records the engagement and ties the resulting transaction to that rep. The rep then earns a flat $10.00 commission regardless of the order total. A $50 sale and a $500 sale both pay the same fixed amount. This keeps your cost structure predictable and your reps focused on closing volume rather than upselling. Attribution uses a "newest engagement wins" model. If a customer somehow uses two different rep codes on the same order (uncommon, since WooCommerce typically allows one coupon at a time), the most recent code determines who gets credit. In practice, each transaction maps cleanly to one rep. > How do I pay my internal sales team a commission tracked through their unique coupon codes? ### Program Snapshot - Best for: Retail floors, call centers, and dealerships with in-house reps - Main goal: Pay reps a predictable flat amount per closed sale - Partners involved: Your own employees, not external affiliates - Actions tracked: Bound coupon codes used at checkout - Rewards supported: Flat fixed commission per qualifying transaction - Starting point: Start free with Siren Lite ### What This Recipe Configures - Sales Team Commissions: $10.00 fixed per transaction, newest engagement wins attribution, tracked via Coupon codes ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Sales Override Commission Program (https://www.sirenaffiliates.com/recipes/tiered-sales-override-program): The Sales Team Commission Program pays each rep a flat commission on their own deals and stops there, with no one earning on anyone else sales. The Tiered Sales Override Program keeps that own-sale commission and adds an override that flows up a ranked reporting line, so managers above a rep earn on the rep production. Choose the Tiered Sales Override Program when managers should earn an override on their team sales, not just reps earning on their own. ### Frequently Asked Questions **What is a sales commission program?** A sales commission program pays your reps a set reward for each sale they close. In this version, a personal coupon code does the identifying: when a customer checks out with a rep's code, that rep collects a flat commission on the sale. It's built for employees you manage directly, not for outside affiliates or marketers. **How does a sales commission program work?** Each rep hands customers a unique coupon code, on the sales floor, over the phone, or at a service desk. When that code shows up at checkout, Siren ties the order to the rep who owns it and records a flat $10 commission, an amount you can change. You then approve the credited sales and pay out on whatever schedule fits your payroll. **Why does the JSON show 1000 instead of 10 for the commission?** Amounts are stored in cents. 1000 means $10.00. To set a $25 commission, enter 2500 in the payoutPerTransaction field. **How do I assign a coupon code to a sales rep?** Create a WooCommerce coupon and link it to the rep's collaborator profile in Siren. When a customer uses that code at checkout, Siren credits the rep automatically. **Can I use this for in-store sales, not just online?** Yes. If your point-of-sale system feeds orders into WooCommerce, in-store coupon usage will trigger commissions the same way online orders do. **Does the coupon code need to give customers a discount?** No. You can create a WooCommerce coupon with a zero-dollar discount and use it purely for tracking. The code identifies the rep without reducing the order total. ## Split Commission Program Source: https://www.sirenaffiliates.com/recipes/split-commission-program A performance-weighted affiliate program where commissions are divided proportionally among all collaborators who contributed to a sale. Higher engagement scores earn a larger share. Built for partnership marketing with multiple touchpoints. ## What This Recipe Does This recipe creates a single affiliate program with performance-weighted commission splitting. When multiple affiliates contribute to a sale, the total 20% commission is divided proportionally based on each affiliate's engagement score. Collaborators who drove more engagement earn a bigger piece of the payout. This is different from equal-split attribution. In an equal split, three affiliates each get one-third regardless of contribution. In a performance-weighted split, the affiliate who drove five site visits earns a larger share than the one who contributed a single coupon code. The math rewards effort. ## Who It's For - **Partnership marketers** running coordinated campaigns where bloggers drive awareness and deal sites close sales - **Affiliate managers** who find equal splits unfair but still want every contributor rewarded - **Multi-channel brands** where affiliates interact with customers across referral links, coupon codes, and other touchpoints ## How It Works When you apply this recipe, Siren creates a program that tracks two engagement types: referred site visits and bound coupon usage. Each event type carries a point value (100 points by default). As customers move through your store, Siren records every affiliate interaction along the way. When a customer completes a purchase, Siren calculates each contributing affiliate's total engagement score and divides the 20% commission proportionally. If Affiliate A accumulated 300 points (three site visits) and Affiliate B accumulated 100 points (one coupon use), Affiliate A receives 75% of the commission and Affiliate B receives 25%. This proportional approach solves a common frustration with multi-touch programs. Equal splits can feel unfair when one affiliate did most of the work. Winner-take-all models discourage collaboration entirely. Performance-weighted splitting finds the middle ground: everyone who contributed gets paid, and the payout reflects how much they contributed. You can fine-tune the weighting after installation by adjusting the point values on each engagement type. Setting coupon usage higher than site visits, for example, would reward affiliates who close over those who only drive traffic. Commissions are calculated on line items only, so shipping, taxes, and fees are excluded. > Is there a way to split a commission across multiple collaborators weighted by who did the most work? ### Program Snapshot - Best for: Partnership marketing teams where multiple affiliates touch one sale - Main goal: Pay every contributor a share that matches their effort - Partners involved: Content creators, coupon partners, and collaborating affiliates - Actions tracked: Referred store visits plus bound coupon redemptions, each worth points - Rewards supported: One 20% commission divided proportionally by engagement score - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Split Commission Program: 20% percentage of transaction, performance weighted attribution, tracked via Referral links, Coupon codes ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Similar Programs, and When to Use Each - Multi-Touch Sales Attribution (https://www.sirenaffiliates.com/recipes/multi-touch-sales-attribution): Multi-Touch Sales Attribution drops the engagement-score weighting the Split Commission Program runs on and divides a 20% commission into identical shares, one per contributing affiliate. Three contributors each take a third no matter how much effort each one put in. Use Multi-Touch Sales Attribution when you want a flat, dispute-proof split that treats every contributor the same. - Top Performer Affiliate Program (https://www.sirenaffiliates.com/recipes/top-performer-affiliate-program): The Top Performer Affiliate Program never divides a payout. Whichever collaborator's engagement score comes out highest collects the entire 25% commission, with ties broken by the most recent engagement. Go with the Top Performer Affiliate Program if a single-winner contest motivates your affiliates better than shared payouts. ### Frequently Asked Questions **What is a split commission program?** A split commission program divides a single sale's commission among every affiliate who contributed to that conversion. Instead of one winner taking the whole payout, each collaborator receives a portion. In this recipe the portion is weighted: more engagement earns a bigger slice of the 20% pool. **Who should use a split commission program?** Teams running partnership marketing where conversions rarely come from a single source. Say a blogger warms up the buyer but the final coupon comes from a deal site: under single attribution only one of them gets paid. A weighted split rewards both, so neither partner has a reason to stop collaborating. **How is the commission split calculated?** Each collaborator's share is proportional to their engagement score. If Affiliate A has 300 points and Affiliate B has 100 points, A receives 75% of the commission and B receives 25%. **How is this different from the Multi-Touch Sales Attribution recipe?** Multi-Touch splits the commission equally among all contributors regardless of effort. This recipe weights the split by engagement score, so affiliates who contribute more earn a larger share. **What happens if only one affiliate engaged with the customer?** That affiliate receives the full commission. The weighted split only applies when multiple collaborators contributed to the same conversion. **Can I adjust the point values for site visits versus coupon usage?** Yes. After installing the recipe, edit the engagement types in your Siren admin to change their point values. Setting coupon usage to 200 and site visits to 100, for example, would give coupon-based closers twice the weight per interaction. ## Team Performance Bonus Source: https://www.sirenaffiliates.com/recipes/team-performance-bonus A team performance bonus program for WooCommerce. A share of your qualifying revenue funds a monthly bonus pool, and each use of the team lead's coupon code credits the reps on their team, so Siren splits the pool among them with the people closest to the lead earning the largest share. The straightforward way to run a team revenue-share bonus instead of paying outside affiliates. ## What This Recipe Does This recipe pays your sales team a monthly bonus that the team lead earns for them. A share of your store's qualifying revenue funds a bonus pool, and each use of the team lead's coupon code credits the reps on their team. Every month, Siren splits that pool among the credited reps, with the people closest to the lead earning the largest piece and the share getting smaller for each position further down. It turns a team lead's selling into a reward for the whole team they manage, instead of sending commission out to external affiliates. The lead's coupon-tracked selling is what brings their reports into the bonus, and each rep's position on the team decides how much they share. The bonus is ready to run as soon as you apply the recipe. Replace the example team members with your own people, set how much of the revenue funds the pool and how the shares are weighted, and the monthly bonus starts paying out on schedule. ## Who It's For - **Sales managers** who want to reward the reps on their team with a monthly bonus pool - **Department heads** sharing a slice of their team's revenue back to the people who deliver it - **Team leads** who want their closest reports to earn the biggest piece of the bonus - **Channel and dealer sales leads** whose results arrive as a monthly sell-through report rather than a stream of checkout orders ## How It Works You build the team as a simple [ordered chain](/documentation/collaborator-group-structures/linear-chain), with the team lead at the top and their reps below them in order. The order matters because it decides who earns the most: the rep directly below the lead earns the biggest share, and each position further down earns less. Two things happen as your store runs. First, Siren sets aside a share of your qualifying revenue into a bonus pool. The default is 10% of the product revenue on your completed orders since the last payout, with shipping, taxes, and fees left out. Second, when the team lead's code is applied at checkout, Siren [credits the reps below them on the chain](/documentation/calculation-strategies/downline-cascade), giving the most to the rep nearest the lead and less to each one further down. That crediting is what decides who shares the pool and in what proportion. The two run on different clocks: the pool only grows from orders that complete, while team credit lands as soon as the code is applied. The lead's deals do not all have to close at your checkout. If they come in by phone or land on a monthly sell-through report, enter or import them as orders, turn on the [manual attribution](/documentation/general/manual-attribution) trigger alongside the coupon trigger, and attribute each one to the lead, by hand in your Siren admin or over the REST API, and the team is credited the same way the lead's code credits them at checkout. At the end of each month, Siren pays the pool out to the reps who were credited, with each rep's slice sized by where they sit in the chain. You choose how many reps deep the bonus reaches, up to five, and how much weight each position carries. You size the shares with simple point weights rather than fixed dollar amounts, so every payout scales with how much the pool actually earned that month. With the default weights, the three reps closest to the lead split the pool 4 to 2 to 1, so the nearest rep takes home twice what the next one does. Adjust the weights to make the split as flat or as steep as you want, and set any position to zero to stop the bonus there. The [configure cascade payouts](/documentation/getting-started/configure-cascade-payouts) guide covers tuning the per-position weighting. The team lead's code drives the bonus but the lead is not paid from the pool. This program rewards the people they manage. If you also want to pay the lead directly on their own sales, add a separate commission program for them and run it alongside this one. Siren handles both at the same time. If a rep is inactive, Siren skips them and they earn nothing, but their slot in the chain stays put. The reps below keep the weights of their own positions, and the share the inactive rep would have claimed spreads proportionally across the active reps who were credited. To actually move people up, change their positions in the collaborator group. Refunded or cancelled sales are subtracted from the pool's revenue before it pays out, so the bonus reflects revenue that actually stuck. ## When To Use This Use this recipe when you want to motivate a whole team, not just one closer. A standard commission rewards the individual who made the sale. A team performance bonus rewards the people around them too, which encourages mentoring, shared accounts, and the kind of teamwork that flat commission plans tend to discourage. It is a strong fit for inside sales teams, account pods, and any group where a senior lead drives revenue the junior members help support. For a deeper look at the pieces behind this recipe, see [what is a cascade](/documentation/general/what-is-a-cascade) for how the lead's code credits the team, the [performance-weighted pool](/documentation/distribution-structures/performance-weighted-pool) for how the pool is split, and [create a revenue share](/documentation/getting-started/create-a-revenue-share-in-siren) for setting up the bonus itself. > How do I pay my sales team a monthly bonus pool from a share of revenue, with the reps closest to the team lead earning a bigger share? ### Program Snapshot - Best for: Sales managers rewarding a whole team, not one closer - Main goal: Fund a monthly team bonus from a share of store revenue - Partners involved: The reps below the team lead in an ordered chain - Actions tracked: The team lead's coupon code applied at checkout - Rewards supported: Monthly pool shares sized by per-layer point weights - Starting point: Start free with Siren Lite ### What This Recipe Configures - Distributor: Team Bonus Pool ### Similar Programs, and When to Use Each - Instructor Team Revenue Share (https://www.sirenaffiliates.com/recipes/instructor-team-revenue-share): The Instructor Team Revenue Share fires its downline cascade on courseCompleted events from a lead instructor's LifterLMS or LearnDash courses, where the Team Performance Bonus cascades each time the lead's coupon code is applied at checkout. The Instructor Team Revenue Share also funds a larger default pool, 30% of qualifying revenue against the Team Performance Bonus's 10%. Pick the Instructor Team Revenue Share when the team being paid is a lead instructor's supporting teachers and course completions, not orders, should drive the split. - Monthly Sales Bonus (https://www.sirenaffiliates.com/recipes/monthly-sales-bonus): Where the Team Performance Bonus divides its monthly pool across every credited rep in the chain, the Monthly Sales Bonus awards the entire pot to whichever competitor finishes the month on top, then everyone starts the next round from zero. Competitors in the Monthly Sales Bonus build their own scores from referred visits or coupon redemptions, with no team chain involved. Run the Monthly Sales Bonus when you want a head-to-head contest where one top performer claims the whole prize each month. - Sales Override Commission Program (https://www.sirenaffiliates.com/recipes/tiered-sales-override-program): The Sales Override Commission Program settles on every individual sale: the closing rep keeps a 10% commission and a fixed $50 override pool relays up the chain to the managers above them. By contrast, the Team Performance Bonus accrues one revenue-funded pool all month and cascades credit downward from the team lead's coupon activity to the reps below. When managers expect a predictable per-deal override paid as each rep closes, not a share of a monthly pool, lean on the Sales Override Commission Program. ### Frequently Asked Questions **What is a team performance bonus?** A team performance bonus is a reward shared by an entire sales team instead of paid to the one person who closed the deal. A slice of revenue funds a pool, and the team splits it on a set schedule, weighted by each member's place in the team. In this recipe, the team lead's coupon activity decides who shares the pool, and the reps nearest the lead take the largest cut. **Who should use a team performance bonus?** Teams where revenue is a group effort: inside sales pods, account teams, and departments where a senior closer depends on the people supporting them. If flat commission keeps paying one closer while everyone else watches, a shared pool rebalances the incentive toward mentoring and teamwork. It fits best when the team has one clear reporting line, since each rep's share follows their distance from the lead. **What funds the bonus pool, and who gets paid from it?** The pool is funded by a share of your store's qualifying revenue, 10% by default, accrued since the last payout. The reps on the team get paid from it. The team lead's selling is what puts that bonus in reach of their reps: each time a customer applies the lead's code, the reps below the lead get credited, which is how Siren decides who shares the pool and in what proportion. The lead is not paid from it. **What counts toward the pool, and how are the lead's sales tracked?** The pool grows by a share of the product revenue on your completed qualifying orders, with shipping, taxes, and fees left out. Team credit works differently: it lands as soon as the lead's code is applied to a cart, before the order completes. Sales without the lead's code still add to the store revenue that funds the pool, but they do not credit the team. **Our sales close off-site and arrive as a monthly report. Can the bonus still run?** Yes. Bring those sales in as orders, enable the manual attribution trigger on the bonus alongside the coupon trigger, then attribute each order to the team lead from your Siren admin or over the REST API. Siren's commission tracking treats an attributed order like a coupon-tracked one, so the revenue grows the pool and the attribution credits the team. See the manual attribution guide, linked below. **How does Siren decide how much each rep gets?** You give each position on the team a point weight, and Siren splits the pool in proportion to those weights. With default weights of 100, 50, and 25, the three reps closest to the lead split the pool 4 to 2 to 1. The points are just a way to size each share, not fixed dollar amounts, so the actual payout scales with how much the pool earned that month. **How many people on the team can earn from the bonus?** Up to five reps below the lead can earn a share. You choose how many levels deep the bonus reaches and how much each level earns. Set a level's weight to zero to stop the bonus there. **Does the team lead earn a share of the pool too?** Not from this program. The lead funds the pool and shares it down to their reps. If you also want to pay the lead directly on their own sales, add a separate commission program for them alongside this one. Siren runs both at the same time. **What happens if a team member is inactive?** An inactive rep is skipped and earns nothing, but their spot in the chain stays occupied, so the reps below them keep their own layer weights instead of moving up. The unclaimed share isn't handed to the next rep down. It spreads proportionally across the active reps who were credited. If you want the team to re-rank, update the positions in the collaborator group. **How and when does the team get paid, and what about refunds?** The pool pays out on the schedule you set, the first of each month by default, and you pay the reps from Siren on your own schedule. Each rep can see their share in the collaborator dashboard. Refunded or cancelled sales are subtracted from the pool's revenue before it pays, so the bonus reflects revenue that actually stuck. See the how to pay collaborators and collaborator dashboard guides, linked below. **Do I need WooCommerce for this to work?** Yes. Siren funds the bonus pool from your WooCommerce sales, so WooCommerce must be installed and active. Easy Digital Downloads and North Commerce are also supported. **Does this require a specific Siren plan?** Yes. The team revenue-share bonus is a Siren Pro feature. **Can I run this alongside my other programs?** Yes. Siren supports multiple programs at once, so a team bonus can run right next to a standard affiliate program or a direct sales commission without interfering. ## Tiered Affiliate Program Source: https://www.sirenaffiliates.com/recipes/tiered-affiliate-program An affiliate program with two commission tiers. Standard affiliates earn 10% while top performers are promoted to a VIP tier earning 25%. Uses a program group to ensure only one tier fires per sale. ## What This Recipe Does This recipe creates two affiliate programs bundled in a program group: 1. **Standard Affiliate** - 10% commission on referred sales 2. **VIP Affiliate** - 25% commission on referred sales Both programs track referral link visits and calculate commissions the same way. The difference is the payout rate. A program only pays affiliates enrolled in it, so each sale pays the tier its affiliate belongs to. The program group covers sales that match both tiers, such as an affiliate enrolled in both during a transition, so only one program fires. Use this recipe when a flat commission rate is not enough to motivate your top performers and you want a clear promotion path that rewards affiliates for growing with you. ## Who It's For - **Growing affiliate programs** that want to reward top performers without overpaying new affiliates - **Stores with established affiliate relationships** where some partners consistently drive high volume - **Program managers** who need a clear, motivating promotion path for their affiliates ## How It Works Every affiliate starts in the Standard tier, earning 10% on the sales they refer. As they prove themselves by hitting volume targets, consistently driving quality traffic, or meeting whatever criteria you set, you promote them to VIP by moving them into the higher-tier program. VIP affiliates earn 25% on the same kinds of sales. The program group covers the overlap. A program only pays affiliates enrolled in it, so on an ordinary sale the group has nothing to decide. Bundling both programs tells Siren they are tiers of the same system rather than programs meant to stack, so when a sale matches both tiers, because an affiliate sits in both during a transition or two affiliates from different tiers referred the same customer, only one commission fires, and the most recently triggered engagement takes priority. You are not limited to two tiers. After applying this recipe, you can add a third or fourth tier to the group with different rates. For example, you could create a Silver tier at 15% between Standard and VIP. The group handles the mutual exclusivity regardless of how many tiers you add. > How do I run a tiered affiliate program where top performers automatically earn a higher rate? ### Program Snapshot - Best for: Affiliate programs ready to reward proven, high-volume partners - Main goal: Motivate affiliates with a promotion path to higher rates - Partners involved: Standard affiliates plus a VIP tier of top performers - Actions tracked: Referral link visits and the sales they produce - Rewards supported: Percentage commissions at two rates, 10% and 25% - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Standard Affiliate: 10% percentage of transaction, newest engagement wins attribution, tracked via Referral links - VIP Affiliate: 25% percentage of transaction, newest engagement wins attribution, tracked via Referral links - Program group Affiliate Tiers: newest engagement wins across standard, vip ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. - Reporting-line overrides (Siren Pro): Pay an override up or down a real reporting line when someone makes a sale, so a manager or senior partner earns on their team’s production. ### Frequently Asked Questions **What is a tiered affiliate program?** A tiered affiliate program pays different commission rates depending on an affiliate's standing. Everyone starts at a base rate, 10% in this recipe, and your strongest partners get promoted to a higher one, here 25%. The better rate gives affiliates a concrete reason to push for more volume instead of coasting. **How does a tiered affiliate program work?** Each tier is a separate program with its own commission rate, and you promote affiliates by moving them from one to the next. A program only pays affiliates enrolled in it, so each sale pays the tier its affiliate belongs to. Tracking doesn't change as affiliates move up. They share the same link either way, and when a referred visit turns into a sale, the commission credits whichever tier they're currently in. The program group covers the sales that match both tiers, such as an affiliate sitting in both during a promotion or a customer referred by a Standard affiliate and later by a VIP affiliate, and makes sure only one tier pays. **How do I promote an affiliate from Standard to VIP?** Move them between programs in the Siren admin. Remove them from Standard and add them to VIP. The program group handles the transition cleanly if they briefly appear in both. **Can I have more than two tiers?** Yes. Add more programs to the group with different rates. The program group handles mutual exclusivity for any number of tiers. **What does the program group actually do?** It prevents double-paying on a sale that matches both tiers. A program only pays affiliates enrolled in it, so with every affiliate in exactly one tier and one referrer per sale, only that tier pays. When an affiliate sits in both tiers during a promotion, or a customer was referred by a Standard affiliate and later by a VIP affiliate, both programs would fire. The group makes sure only one does. **Does this require a specific Siren plan?** Yes, as written. The recipe installs a program group, which is an Essentials feature. The two-rate structure itself runs on Lite. Create the Standard and VIP programs yourself, enroll each affiliate in one of them, and each sale pays the tier its affiliate belongs to. Two things differ on Lite. A sale referred by a Standard affiliate and later by a VIP affiliate pays both of them, where the group would pick one. And when you promote someone, remove them from Standard before adding them to VIP, so no sale lands while they sit in both. Essentials adds the group, which handles both cases for you. ## Tiered Affiliate Program with Overrides Source: https://www.sirenaffiliates.com/recipes/tiered-affiliate-cascade A tiered affiliate program for WooCommerce where senior affiliates earn an override on the sales their juniors refer. The affiliate who refers the sale earns a flat commission, and a separate override amount is split among the partners ranked above them, with the largest cut going to whoever sits closest above the seller. Promote an affiliate by ranking them higher, with no separate program to manage. ## What This Recipe Does This recipe launches a tiered affiliate program where senior affiliates earn an override on the sales their junior affiliates refer. When an affiliate refers a sale, the affiliate who made that referral earns a flat commission on it, $20 by default. On top of that, a separate override amount, $10 by default, is split among the partners ranked above them, with the biggest cut going to whoever sits closest above the seller. The override is a set amount per sale rather than a percentage, so your cost per referred sale is predictable. The result is a program where a partner's pay follows the standing you've given them. Rank someone near the top, for the channel they built or the relationships they manage, and they earn an override on what the chain below them refers, on top of their own flat commission. You decide how much the override is worth, how many levels of seniority earn it, and how the cut shrinks at each level. Promoting an affiliate is as simple as ranking them higher, with no separate program to set up or maintain. ## Who It's For - **Affiliate program owners** who want senior partners to earn a share of the sales their junior affiliates refer, all from one program - **Program managers** who want to promote affiliates by rank, without the overhead of running a separate program for every tier - **Partner programs** where the override should follow an affiliate's standing in your roster rather than which plan or program they happen to be in ## How It Works Think of your affiliates as ranked from top to bottom in a chain you define. Every affiliate has a flat commission they earn on the sales they personally refer. The override sits on top of that and rewards the partners ranked above the seller. Each affiliate gets their own [referral link](/documentation/general/site-visited), and a sale is credited to whoever referred the customer within the program's attribution window. When a ranked affiliate refers a sale, two payouts happen at once. First, that affiliate earns their flat $20 commission on the sale, exactly like a standard affiliate program. Second, a $10 override on the same sale is split among the partners ranked above them in the chain. The partner directly above the seller takes the largest share, and each rank higher up takes a smaller cut. The seller is always paid for their own work, and the override is a separate reward that flows up to their seniors. The seller never earns a piece of their own override. You control both the size of the override and how it is divided. The override is a set amount per sale, and each level above the seller is assigned a share that splits it. For example, a $10 override with shares of 100, 50, and 25 splits 4 to 2 to 1 among the first three levels above the seller, paying roughly $5.71, $2.86, and $1.43, so the closest senior partner earns the most. The override can reach up to five levels above the affiliate who refers the sale. Set a level's share to 0 and the override stops there, so you only ever pay out as many levels as you choose to fund. Tiers come entirely from rank. A partner ranked near the top earns the largest override share on every sale referred by the affiliates below them. To promote someone, you simply rank them higher. There is no second program to move them into and no window where they briefly sit in two tiers at once. If a senior partner is paused or inactive, the override is split among the remaining active partners above the seller, each at their own level's weight, so none of it is wasted. ## A Quick Example Picture three affiliates ranked Riley at the top, then Morgan, then Jordan. When Jordan refers a sale, Jordan earns the flat $20 commission, and the $10 override is split between the two partners above them, so Morgan takes the first-level share and Riley the smaller second-level share (about $6.67 and $3.33 with the default weights). When Morgan refers a sale, Morgan earns the flat $20 and Riley takes the whole $10 override as the only partner above. Riley sits at the top with no one above them, so a sale Riley refers pays only Riley's own flat $20 commission. To move Jordan up, you raise their rank, and the override picture updates automatically from that point on. ## Applying This Recipe Apply the recipe to set up the whole program in one step, including the flat affiliate commission, the override, and a sample roster of ranked partners. After applying it, swap the example partners for your own affiliates, set each one's rank, and adjust the flat commission and override shares to your numbers. For a deeper look at how tiered overrides work, see [what is a cascade](/documentation/general/what-is-a-cascade), [ranked partner groups](/documentation/collaborator-group-structures/linear-chain), and [override payouts](/documentation/calculation-strategies/upline-cascade). If you already run an older two-program tiered setup, the [migration guide](/documentation/migration/migrate-tiered-program-group-to-cascade) walks through moving to this one. > How do I pay senior affiliates a share of the sales their junior affiliates refer? ### Program Snapshot - Best for: Affiliate programs where seniors earn on their juniors' referred sales - Main goal: Pay senior partners an override that matches their standing - Partners involved: Affiliates ordered in a linear chain, seller plus their upline - Actions tracked: Referral link visits and the purchases they turn into - Rewards supported: Flat commission per sale plus a layer-weighted override pool - Starting point: Start free with Siren Lite ### What This Recipe Configures - Affiliate Referral Commission: $20.00 fixed per transaction, newest engagement wins attribution, tracked via Referral links - Upline Override Pool: $10.00 fixed per transaction, performance weighted attribution, tracked via Referral links ### Similar Programs, and When to Use Each - Tiered Affiliate Program (https://www.sirenaffiliates.com/recipes/tiered-affiliate-program): The Tiered Affiliate Program builds its tiers from a program group, one program per commission rate at 10% and 25%, and a sale only ever pays the affiliate's own bracket. Nothing flows between partners in the Tiered Affiliate Program, while the Tiered Affiliate Program with Overrides pays senior affiliates a slice of each sale their juniors refer. Stick with the Tiered Affiliate Program when senior partners just need a better rate on their own referrals and nobody earns on anyone else's sales. - Sales Override Commission Program (https://www.sirenaffiliates.com/recipes/tiered-sales-override-program): Built for internal teams, the Sales Override Commission Program trades the referral links of the Tiered Affiliate Program with Overrides for personal coupon codes, pays the closing rep 10% of the order instead of a flat $20, and runs a $50 override pool per sale rather than $10. If the chain is your own reporting line of employees and reps close deals by phone, email, or on the sales floor, pick the Sales Override Commission Program. ### Frequently Asked Questions **What is a two-tier affiliate program?** A two-tier affiliate program pays on two levels of activity. An affiliate is paid for the sales they refer directly, and the partner ranked above them earns an override on those same sales. In this recipe the seller takes a flat commission while a separate override is split among their upline, and the chain can carry that override up to five levels if you want more than two tiers. **Who should use a two-tier affiliate program?** Programs where partner seniority should show up in the pay. If a partner earned a high rank because they built your channel, manage key relationships, or bring strategic reach, the second tier pays them an override on the sales referred below their position. Some programs also let senior partners introduce new affiliates, but the override follows the ranking you maintain, not who introduced whom. It also fits programs that promote partners over time, since moving someone up the chain widens their override reach without creating another program to manage. **How do I promote an affiliate to a higher tier?** You rank them higher in your affiliate list. The override rate follows an affiliate's rank, so moving someone up automatically gives them a cut of the sales referred by everyone now below them. There is no separate program to add them to and no window where they sit in two tiers at once. **Does the affiliate who refers the sale earn anything from the override?** Yes, but not from the override itself. The affiliate who refers the sale earns their own flat commission on that sale. The override is an extra payout that goes only to the senior affiliates ranked above them, so the seller and the partners above them are all paid for the same sale. **Is the override a percentage of the sale or a fixed amount?** It is a fixed amount per referred sale, not a percentage, so it does not change with the order total. The default is a $10 override on each sale a junior refers, and you can set that amount to whatever you want. The referring affiliate's own commission is also a flat amount, $20 by default. **How do I set how much each level earns?** Two settings control it. First you set the total override amount per sale, $10 by default. Then you give each level above the seller a share, and Siren splits that total in proportion to those shares. A setting of 100, 50, and 25 splits the override 4 to 2 to 1 among the first three levels above the seller, so the closer a partner sits to the seller, the larger their cut. You decide both the total and the split. **How many levels of seniority can earn an override?** Up to five levels above the affiliate who refers the sale. The first level is the partner directly above the seller, and each level after that is one rank higher. Set a level's share to 0 to stop the override there, so you only pay out as many levels as you choose to fund. **What happens if a senior affiliate is paused or inactive?** They are skipped on that sale, and the gap is not filled. The partner above a skipped member keeps their own level and weight rather than dropping into the empty slot, and the skipped member's portion spreads across the remaining active partners in proportion to their shares. No part of the override is lost to an inactive affiliate. **How does Siren know which affiliate referred a sale?** Each affiliate gets their own referral link, and when a customer arrives through that link and buys within the program's attribution window, Siren credits that affiliate with the sale and runs the override up the chain above them. You set the window length when you create the program. See the referral tracking guide, linked below. **How and when does everyone get paid, and what about refunds?** Siren tracks what the seller and each senior affiliate earned, and you pay them on your own schedule. Everyone can see their earnings in the collaborator dashboard. Because credit follows real sales, a refunded or cancelled order reverses the commission for that sale, including the overrides paid up the chain, so a sale that did not stick does not stay paid. See the how to pay collaborators and collaborator dashboard guides, linked below. **Does this require a specific Siren plan?** This recipe needs Siren Pro, because the ranked chain and the automatic override relay are the Pro features. The tiers themselves don't require Pro. If you only want two rate brackets where each affiliate earns more on their own sales, one program per bracket does that on any plan, including Lite, and the program-group version that keeps the brackets from paying twice on one sale runs on Siren Essentials. See the Tiered Affiliate Program recipe, linked below, for that shape. ## Top Performer Affiliate Program Source: https://www.sirenaffiliates.com/recipes/top-performer-affiliate-program A competitive affiliate program where only the highest-scoring collaborator earns the commission. When multiple affiliates engage with the same customer, the one with the deepest cumulative engagement wins. Rewards quality over quantity. ## What This Recipe Does This recipe creates a single affiliate program with competitive, top-scorer attribution. When multiple affiliates engage with the same customer before a purchase, only the collaborator with the highest cumulative engagement score earns the 25% commission. Everyone else gets nothing for that transaction. This model rewards affiliates who invest in sustained, meaningful engagement rather than one-off link shares. It is built for programs where you want to motivate quality over quantity. ## Who It's For - **Brands running competitive affiliate programs** that want to identify and reward their best performers - **Store owners with high-value products** where deep customer engagement matters more than sheer referral volume - **Affiliate managers** who want clear, single-winner attribution with no commission splitting ## How It Works When you apply this recipe, Siren creates a program that tracks two engagement types: referred site visits and bound coupon usage. Each engagement type carries a point value (100 points by default). As affiliates interact with customers through links and coupon codes, Siren tallies their engagement scores per customer. When a customer completes a purchase, Siren compares the scores of all collaborators who engaged with that customer. The collaborator with the highest cumulative score wins the full 25% commission. If one affiliate drove three separate site visits (300 points) and another provided a single coupon code (100 points), the first affiliate takes the commission. This "top score wins" model creates natural competition among your affiliates. Those who consistently engage with customers across multiple touchpoints will outperform those who rely on a single interaction. The result is a program that rewards the affiliates doing the most work to convert your customers. Commissions are calculated on line items only, so shipping, taxes, and fees are excluded. > How can I make sure only the highest-scoring affiliate wins the commission on a sale? ### Program Snapshot - Best for: Competitive programs that reward engagement depth over referral volume - Main goal: Push affiliates to out-engage each other on every sale - Partners involved: Affiliates competing head-to-head for single-winner commissions - Actions tracked: Referred site visits, bound coupon use, completed purchases - Rewards supported: Percentage commission paid to the top scorer only - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Top Performer Program: 25% percentage of transaction, top score wins attribution, tracked via Referral links, Coupon codes ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Lifetime commissions (Siren Plus): Keep crediting a collaborator on every future order from customers bound to them, automatically and ongoing, with no repeat click or coupon. - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Similar Programs, and When to Use Each - Multi-Touch Sales Attribution (https://www.sirenaffiliates.com/recipes/multi-touch-sales-attribution): Multi-Touch Sales Attribution shares the commission rather than awarding it to one winner. Every affiliate who logged a referred visit or a bound coupon use for the customer gets an equal slice of the 20% pool, with no scoreboard deciding the outcome. Pick Multi-Touch Sales Attribution when your affiliates collaborate on the same customers and a winner-take-all payout would punish that. - Split Commission Program (https://www.sirenaffiliates.com/recipes/split-commission-program): The Split Commission Program pays every contributor, not only the top scorer. Each affiliate's engagement points convert into a proportional cut of a 20% commission, so a 300-point affiliate earns three times what a 100-point affiliate does on the same sale. Choose the Split Commission Program when you want scores to size each payout instead of deciding a single winner. ### Frequently Asked Questions **What is a top performer affiliate program?** It's an affiliate program that pays the full commission to exactly one affiliate per sale: the one with the highest engagement score for that customer. Instead of splitting credit or defaulting to the last click, Siren scores every tracked interaction and lets the deepest relationship win. An affiliate who kept showing up for a customer beats one who dropped a single link. **Who should use a top performer affiliate program?** Brands that want competition doing the motivating. It fits high-value products where a customer hears from several affiliates before buying, and managers who want one clear winner instead of split credit. If your affiliates collaborate more than they compete, multi-touch attribution is the better match. **How does Siren determine the top scorer?** Each engagement type has a point value. Siren adds up all engagement points a collaborator accumulated for a given customer. The collaborator with the highest total score wins the commission. **What happens if two affiliates have the same score?** In a tie, Siren uses the most recent engagement as the tiebreaker. The affiliate who engaged with the customer last among the tied collaborators receives the commission. **Does this discourage affiliates from collaborating?** It can. This model is designed for competitive programs where you want affiliates to go above and beyond. If you prefer collaborative incentives, consider the Multi-Touch Sales Attribution recipe instead. **Can I adjust the point values for each engagement type?** Yes. After installing the recipe, edit the engagement types in your Siren admin to change their point values. Higher values for coupon usage versus site visits, for example, would favor affiliates who close sales over those who only drive traffic. **Does this require a specific Siren plan?** Yes. This recipe uses the Top Score Wins attribution strategy, which is a Siren Essentials feature. The affiliate program, referral and coupon tracking, and manual payouts are all available on the free tier, but Top Score Wins attribution requires Essentials. ## Travel Destination Marketplace Source: https://www.sirenaffiliates.com/recipes/travel-destination-marketplace A revenue-share program for travel curators reselling bookings on behalf of small vacation rental destinations. Each destination earns 90% of every booking on their listing, with the curator keeping 10% to cover marketing and operations. ## What This Recipe Does This recipe runs a commission split where each destination keeps 90% of *every* booking sold through the marketplace. The curator keeps 10% to cover marketing, payment processing, and platform operations. Each destination is added as a [collaborator](/documentation/general/what-is-a-collaborator) and bound to the listing or listings they own on the site. When a guest checks out, Siren sees which destination owns the product and credits the 90% to them automatically. No coupon codes. No referral links. Attribution rides on product ownership. This is the vacation-rental specialization of the generalized [marketplace vendor commission](/recipes/marketplace-vendor-commission) pattern, narrowed to the case where each listing has exactly one owner. ## Building a Boutique Vacation Rental Marketplace A small retreat on a back road, a boutique B&B with eight rooms, an off-grid cabin that only books eight months a year. These are the kinds of places that get buried on Airbnb and can't realistically pay around 15% commission on top of running their own marketing. They have the rooms and the hospitality. What they don't have is audience, brand, or the time to build either one. A travel curator with a story-driven storefront brings exactly that. The curator's job isn't to run rentals. The curator's job is to send the right guests to a hand-picked portfolio of places that share an aesthetic, a region, or a point of view. The 90/10 split fits that division of labor. Destinations do the hospitality. The curator handles the marketplace, the audience, and the brand. This isn't a vacation rental channel manager. A channel manager pushes your listings to Airbnb, Booking, and VRBO and keeps calendars in sync across them. This recipe is for when you *own* the marketplace. Your destinations book through your site, not someone else's. The other big OTAs are competitors, not channels, and the curated brand you've built is the whole reason a guest chose your storefront in the first place. Same pattern applied to courses instead of stays lives in the [online course platform starter](/recipes/online-course-platform-starter). ## Who It's For - **Travel curators** building a story-driven boutique marketplace where they resell bookings for a hand-picked portfolio of vacation rentals, retreats, or boutique stays - **Tour operators or destination collectives** running a website that books experiences across multiple small properties and need automated commission attribution so they aren't running spreadsheets each month - **WordPress site owners** starting an online travel agency or boutique booking platform without running an OTA channel manager or building reservation infrastructure from scratch ## How It Works Each destination is added as a collaborator and bound to one or more booking listings. When a guest checks out, the [`collaboratorProductSold`](/documentation/general/collaborator-product-sold) engagement fires and Siren credits the 90% to the destination that owns the listing. The `newestBindingWins` resolver applies, but because each listing has exactly one owner in this model, the resolver is a tie-break that never fires in practice. If a listing ever transfers to a new destination, the newest binding wins for future bookings. The broader setup context lives in the [marketplaces guide](/documentation/getting-started/marketplaces). Destinations get the default Siren collaborator dashboard out of the box. They can sign in, see their bookings, see their accrued earnings, and review payout history without any custom development on your end. If you want a branded portal that matches your marketplace look and feel, Siren's [REST API](/documentation/resource-reference/introduction) exposes the same data. That kind of branded build is custom work you take on, not something Siren ships pre-made. Siren tracks each destination's accrued commission balance as bookings come in. The curator initiates payouts from the accrued balance on whatever cadence they choose. Monthly is a common rhythm, but the operator decides. Siren doesn't execute payouts automatically. It tracks the balance and gives you the bookings behind it, and you trigger payment through whatever rail you use (Stripe Connect, ACH, PayPal, manual transfer, whatever fits). The [pay collaborators](/documentation/getting-started/how-to-pay-collaborators) doc walks through the flow. On a $200 booking, the destination earns $180 and the curator keeps $20. The $20 covers marketing, payment processing, and platform operations. Commission is calculated on line item totals only, so taxes, separately-itemized cleaning fees, and shipping aren't part of the commissionable amount. The 90% is the default and you can tune it via the customizable field when applying the recipe. The destination keeps *most* of every booking because the labor split tilts the same way. > How do I run a curated vacation rental marketplace that pays destinations 90% of every booking automatically? ### Program Snapshot - Best for: Travel curators reselling bookings for hand-picked vacation rentals - Main goal: Pay each destination 90% of bookings on its listings automatically - Partners involved: Small vacation rental destinations, retreats, and boutique stays - Actions tracked: Bookings sold on owned listings, via the collaboratorProductSold event - Rewards supported: Percentage-of-sale revenue share, calculated on line item totals - Starting point: Start free with Siren Lite. This program runs on Siren Essentials ($229/yr). ### What This Recipe Configures - Destination Revenue Share: 90% percentage of transaction, newest engagement wins attribution, tracked via Owned product sales ### Optional Upgrades Not required to run this program. Each unlocks more at a higher tier: - Reusable collaborator group (Siren Plus): Manage who is eligible from one reusable roster you bind to the program, instead of enrolling collaborators one at a time. ### Frequently Asked Questions **What is a travel marketplace commission?** It's the revenue split between a marketplace operator and the property owners selling through the site. In this recipe each destination keeps 90% of every booking on its own listings, and the curator keeps 10% to cover marketing, payment processing, and platform operations. That percentage is the core economics of the program, so it's exposed as a customizable field when you apply the recipe. **How does a travel marketplace commission work?** Every property owner gets a collaborator record tied to the listings they own. When a guest books, Siren sees which destination owns that listing and credits their share automatically, with no referral links or coupon codes involved. Balances accrue per destination, and the operator pays them out on whatever cadence fits the business. **What if a destination owns multiple listings?** Each listing is bound to its destination collaborator. A destination can own as many listings as it has properties. Every booking on any of those listings credits the same destination. The 90% applies per booking, regardless of how many listings the destination owns. **Does the 90% include taxes, cleaning fees, and other charges?** No. Commission is calculated on line item totals only. Taxes, separately-itemized cleaning fees, and other order-level fees are excluded from the calculation. This keeps the math predictable for both sides. **Can a destination see their bookings and earnings?** Yes. Every collaborator gets access to the Siren collaborator dashboard, where they can see their bookings, earnings to date, and payout history. If you want a branded experience that matches your marketplace, Siren's REST API lets you build a custom portal on top. **How does the monthly payout work?** Siren tracks each destination's accrued commission balance as bookings come in. Once a month, you initiate payouts from the accrued balance through whatever payment rail you use. Siren doesn't run payouts automatically. It gives you the balance and the bookings behind it, and you control the cadence. --- # Integrations ## Easy Digital Downloads integration Source: https://www.sirenaffiliates.com/integrations/easy-digital-downloads Siren is an Easy Digital Downloads affiliate plugin for affiliate, referral, and revenue-share programs around your digital products, licenses, and memberships. Per-product rates, recurring commissions, automatic payouts. > Can you run an affiliate program on Easy Digital Downloads? Yes. Siren is a WordPress plugin that runs natively inside Easy Digital Downloads, reading order and license events so affiliate, referral, and revenue-share programs are tracked and paid the moment a download sells. Per-product rates, recurring commissions, and automatic payouts. Start free on Lite. ### What Siren does on Easy Digital Downloads - Track downloads, licenses and renewals: Read Easy Digital Downloads orders, license renewals, and refunds as conversion events. (all editions) - Per-product and per-category rules: Pay more on flagship products and less on lower-margin offers. (all editions) - Pay as store credit: Issue automatic store credit that partners redeem at checkout. (all editions) - Pay real money via Stripe Connect: Partners self-onboard Stripe Express and get paid automatically on the Plus tier. (all editions) ### Events Siren can track - New download or sale - License or subscription renewal - Specific-product or bundle sale - Refunds and clawbacks - Sales made off your WordPress site (not supported) ### Rewards Siren can pay - Flat or percentage commission - Per-product or per-category rates - Recurring on renewals - Revenue share with creators - Lead and signup bonuses (Gravity Forms) ### Program types you can run on Easy Digital Downloads - Affiliate program - Customer referral program - Creator revenue share - Distributor or reseller network - Multi-tier partner program (Pro) ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as Easy Digital Downloads and own your data. Tracks EDD orders out of the box. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run Easy Digital Downloads with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Prefer it managed? Siren Cloud runs the same engine for your EDD store, hosted and supported by our team. ### What you'll need - A WordPress site with Easy Digital Downloads active. - For renewal and subscription rewards: the Essentials tier plus EDD recurring payments. - For automatic real-money payouts: Plus (Stripe Connect). - No API keys and no developer setup. It is the same WordPress site. ### Good to know - Recurring commissions require EDD recurring payments and the Essentials tier or higher. - Multi-tier and cascading commissions are a Pro-tier feature. ### Pre-built recipes for Easy Digital Downloads - Affiliate Program (Affiliate program), Lite · Free: Percentage commission on every referred download sale. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program - Refer a Friend (Referral program), Essentials: Reward existing customers who refer new buyers. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program - Content Creator Profit Share (Revenue share), Essentials: Split a monthly revenue pool among your blog writers by the traffic their posts earn. Install from https://www.sirenaffiliates.com/recipes/content-creator-profit-share - Product Royalties (Royalty program), Essentials: Creators earn a percentage when their assigned products sell. Install from https://www.sirenaffiliates.com/recipes/product-royalty-program ### Program guides for Easy Digital Downloads - Run an affiliate program on Easy Digital Downloads: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/affiliate-program - Run a referral program on Easy Digital Downloads: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/referral-program - Run a royalty program on Easy Digital Downloads: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/royalty-program ### Frequently Asked Questions **Will Siren work with my existing Easy Digital Downloads store?** Yes. Siren is designed to connect to an existing Easy Digital Downloads site. You do not have to rebuild your catalog, just connect your store, define your programs, and start tracking affiliate and referral activity. **Do I need a developer to set this up?** Most store owners do not. If you are comfortable installing WordPress plugins and configuring Easy Digital Downloads, you can get Siren online. If you are running a heavily customized stack, we can point you toward best practices. **Can I pay different rates for different downloads or licenses?** Absolutely. You can set commission rules by product, category, or specific offer. That makes it easy to pay more on flagship products or bundles, and less on lower-margin or experimental offers. **Can I run both an affiliate program and a referral program for Easy Digital Downloads?** Yes. Siren lets you run multiple programs in parallel. You might have a classic affiliate program for content creators and a separate referral program for existing customers, all inside the same Easy Digital Downloads affiliate integration. **Does Siren support recurring commissions for subscriptions or memberships?** If you are using recurring payments or membership add-ons with Easy Digital Downloads, you can decide whether commissions are paid on the first payment only or on a chosen number of renewals. **Can I migrate from another Easy Digital Downloads affiliate plugin?** In many cases, yes. You can import partners and start tracking new referrals in Siren right away. What you can bring over historically depends on what your current tool exports, and we can help you evaluate that. **What happens if someone cancels or gets a refund?** Commissions move from pending to payable on your schedule. If an order is refunded within your policy window, you can reject that commission, which prevents it from being paid out, keeping obligations clean for you and your partners. **Is Siren only for affiliates?** No. Siren is built for affiliates, referrers, creators, ambassadors, and joint-venture partners. Think of it as an incentive engine for your Easy Digital Downloads store, not just an affiliate tracker. ## Gravity Forms integration Source: https://www.sirenaffiliates.com/integrations/gravity-forms Siren turns Gravity Forms into a partner-ready engine for signups, paid leads, and form-based sales. Tie key submissions to the partner who sent them and pay for the value they create. > Can you run a pay-per-lead or partner program with Gravity Forms? Yes. Siren connects to the Gravity Forms you already have and decides what each submission means: a partner signup, a paid lead, or a sale. Key submissions get tied to the partner who drove them, so you can pay per qualified lead or per form-based sale. Start free on Lite. ### What Siren does on Gravity Forms - Track form submissions as events: Read connected Gravity Forms submissions as leads, signups, or sales. (all editions) - Capture partner signups: Turn a form into partner onboarding, creating affiliates on submit. (all editions) - Track form payments: When someone pays through a connected form, record the order and attribute it. (all editions) - Pay per qualified lead: Keep a running count of qualified leads a partner sends and pay against it. (all editions) ### Events Siren can track - Form submission (lead or application) - Qualified paid lead - Form payment (Gravity Forms and Stripe) - Partner signup form - Submissions you have not marked qualified (not supported) ### Rewards Siren can pay - Flat bounty per qualified lead - Percentage on form-based sales - Signup and application bonuses - Tiered by lead volume - Manual review before payout ### Program types you can run on Gravity Forms - Pay-per-lead program - Partner or affiliate signup - Lead-gen partner program - Form-based sales commissions - Ambassador program ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as Gravity Forms and own your data. Connect forms in a few clicks. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run Gravity Forms with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Prefer it managed? Siren Cloud runs the same engine for your Gravity Forms site, hosted and supported by our team. ### What you'll need - A WordPress site with Gravity Forms active. - The forms you want to connect, plus a rule for what counts as a qualified lead. - For automatic payouts: Essentials and up. Real-money payouts on Plus (Stripe Connect). - Your CRM and email tools stay where they are. Siren just feeds them richer data. ### Good to know - Siren does not replace your CRM or email automations. It makes sure they receive richer data, like who referred a contact. - Siren can sit behind multiple sources at once, so partners get one record across Gravity Forms, WooCommerce, and more. ### Pre-built recipes for Gravity Forms - Cost Per Lead (Lead-gen program), Essentials: Pay a flat bounty for each qualified lead a partner sends. Install from https://www.sirenaffiliates.com/recipes/cost-per-lead-campaign - Pay-Per-Lead Affiliate (Affiliate program), Essentials: Reward affiliates for qualified leads, not just sales. Install from https://www.sirenaffiliates.com/recipes/pay-per-lead-affiliate-program - B2B Referral (Referral program), Essentials: Reward partners who refer qualified business inquiries. Install from https://www.sirenaffiliates.com/recipes/b2b-referral-program ### Program guides for Gravity Forms - Run an affiliate program on Gravity Forms: https://www.sirenaffiliates.com/integrations/gravity-forms/affiliate-program - Run a lead-gen program on Gravity Forms: https://www.sirenaffiliates.com/integrations/gravity-forms/lead-gen-program ### Frequently Asked Questions **Does this work with the Gravity Forms setup I already have?** Yes. You connect specific forms to Siren and decide what they do: create affiliates, leads, or sales. Most of the time you are wiring in Siren, not redesigning your forms. **Do I need a developer to use this?** If you are already comfortable building forms, setting up feeds, and using Gravity addons, you can usually handle this yourself. If your site is heavily customized or mission-critical, a developer can help tighten things up. **Will this replace my CRM or email automations?** No. Your CRM and email tools stay where all the nurturing happens. Siren simply makes sure those tools receive richer data, like who referred a contact and which partner program they are in. **Can I run pay-per-lead programs with this?** Yes. You can mark forms as paid lead sources, give partners links to those forms, and let Siren keep a running count of qualified leads they send. You decide what counts as qualified, and you pay against that, not guesses. **We sell through Gravity Forms and Stripe. Can Siren still track commissions?** Yes. When someone pays through a connected form, Siren records the order, ties it to the right affiliate, and includes it in your partner reporting and payouts. **What if I use WooCommerce or another platform and Gravity Forms?** That is fine. Siren can sit behind multiple sources at once, so partners get one consistent record of the leads and sales they drive, no matter which front-end you used. **What happens if a lead is junk or a customer asks for a refund?** You are still in control. Leads and form-based sales can be reviewed or adjusted before you pay out, so a junk lead or a refunded order does not have to earn a commission. Siren gives you structure and the final say. It does not force you to pay for bad fits. ## HubSpot integration Source: https://www.sirenaffiliates.com/integrations/hubspot Siren Cloud connects to HubSpot, reads closed deals, and runs partner payouts, customer referrals, and sales incentives on the pipeline you already track. Attribution by your CRM data, not tracking links. Hosted and managed. > Can you run a partner or referral program on HubSpot? Yes, with Siren Cloud. HubSpot is your system of record, and Siren connects to it over the API to read closed-won deals and the contacts and companies behind them. From there it attributes each deal to the partner or referring customer, applies your reward rules, and produces a reconciled statement you pay from. Attribution is assigned through the HubSpot properties you already use, not a tracking link, so it survives the weeks or months a B2B deal takes to close. Best fit for teams running pipeline in HubSpot who want partner and referral payouts handled without spreadsheets. ### What Siren does on HubSpot - Read closed HubSpot deals: Uses deals, amounts, and associated contacts as the source of truth for what to reward and to whom. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Attribute by your HubSpot properties: Ties a closed deal back to the partner or customer who referred it using the deal, company, and contact fields you already use. No tracking links to bolt on. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Reverses on loss: Credit reverses when a deal is marked lost, refunded, or churned, so you only pay on revenue you keep. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Reward partners and customers: Applies your payout rules per partner, tier, or referral, produces a reconciled statement each period, and pays partners directly through Stripe Connect when you want Siren to run the payout. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) ### Events Siren can track - Deal closed-won - Renewal or upsell deal - Deal stage change - Deal lost, refunded, or reversed - New partner contact or company - Open deal that never closes (not supported) ### Rewards Siren can pay - Percentage of deal value - Flat bounty per closed deal - Tiered by partner level - Account credit for referring customers - Ongoing share of renewals and expansion ### Program types you can run on HubSpot - Channel partner and reseller program - Customer referral program - Sales incentive and SPIFF program - Strategic partner revenue share - Ambassador program ### Edition availability - WordPress plugin (self-hosted): Not available yet. Not available. The WordPress plugin runs on your own site and tracks WordPress sales. HubSpot is a hosted CRM, so it connects through Siren Cloud. - Siren Cloud self-serve (coming soon): Not available yet. The hosted, sign-up-yourself edition. HubSpot is connected and run for you on our managed Cloud edition. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud connects to your HubSpot account over its API and runs the program for you, hosted, set up, and supported. ### What you'll need - A HubSpot account you can connect, with read access to deals and contacts. - A Siren Cloud plan, hosted and set up by our team around your reward model. - Your program rules: who earns, how much, and on which deals. - No HubSpot development. We handle the connection and the setup. ### Good to know - Siren reads HubSpot data to calculate what each partner is owed, then pays them directly through Stripe Connect or produces a reconciled statement you pay from through your own process. You choose. - Attribution keys off the contacts and properties you already use, so a partner or referring customer maps cleanly to the deals they influenced. - Open deals are not billable. Siren rewards deals that close-won, and reverses credit when one is lost or refunded. - The same model fits other systems of record. If you run Salesforce, a custom CRM, or Stripe billing, Siren can read those instead. ### Pre-built recipes for HubSpot - Partner Program (Partner program), Cloud: Attribute closed HubSpot deals to partners and pay on what they source. Install from https://www.sirenaffiliates.com/recipes/channel-partner-program - Customer Referral (Referral program), Cloud: Reward customers who refer deals that close in HubSpot. Install from https://www.sirenaffiliates.com/recipes/b2b-referral-program - Sales Incentive (Sales commissions), Cloud: Pay reps a SPIFF or bonus on the deals that matter most. Install from https://www.sirenaffiliates.com/recipes/sales-team-commission-program ### Frequently Asked Questions **Does HubSpot have a built-in partner or referral payout engine?** No. HubSpot tracks contacts and deals, but it has no native way to attribute a closed deal to a partner or referrer and pay them. Teams usually fall back to a spreadsheet. Siren Cloud adds that layer by connecting to HubSpot over the API and turning closed-won deals into accurate, reconciled payouts. **Which HubSpot property or association links a deal to a partner?** Whichever ones you already use. Most teams tag the sourcing partner on the deal, on the associated company, or on a custom property like a referring-partner field. Siren reads those when the deal closes-won and credits the right partner or referring customer. We map this to your exact setup when we connect your account, so you are not rebuilding how you track partners in HubSpot. **Do my partners need tracking links or coupon codes?** No. Because Siren reads the closed deal from HubSpot and attributes it by your CRM data, there are no tracking links to bolt onto a long sales cycle and no cookie that expires before the deal closes. Attribution is assigned through HubSpot, which is what makes it hold up over the weeks or months a B2B deal takes. **How do partners actually get paid?** Siren reads your closed deals to calculate what each partner is owed, then pays them directly through Stripe Connect, real money out to each partner, with the reconciled statement behind every payment. If you would rather run payouts through your own bank, PayPal, or finance process, Siren produces the statement and you pay from it. You choose. **How is this different from a SaaS affiliate tool, a partner platform, or a spreadsheet?** Most affiliate tools attribute at a click or a checkout, which fits self-serve signups but not a CRM-driven B2B deal. A spreadsheet can track closed deals, but someone reconciles it by hand every period and nothing reverses on its own. Siren rewards off the closed-won deal HubSpot already tracks, attributed by the contacts and properties you use, with credit reversed on loss, hosted and managed by our team. There is no percentage skimmed from each deal you pay on. If you need a full PRM with deal registration, co-sell marketplaces, and partner-tier marketing portals, we may not be the fit. If you need closed HubSpot deals turned into accurate, paid-out payouts, that is exactly what this does. **What does a HubSpot partner program with Siren cost?** Siren Cloud is custom-priced around your program: a one-time setup to connect HubSpot and model your reward rules, plus an ongoing managed subscription. There is no percentage skimmed from each deal you pay on, so your cost does not balloon as your program grows. Tell us how your program should work and we will scope real numbers. ## Influence FM integration Source: https://www.sirenaffiliates.com/integrations/influence-fm Influence FM is where account ownership lives, so it is the attribution layer Siren credits sales against. Pair it with your billing system and Siren pays each rep under your real plan: tiers, splits, and clawbacks. > Can Siren use Influence FM to run rep commissions? Yes, with Siren Cloud. Influence FM is the CRM where account ownership lives, so it is the attribution layer: it tells Siren which rep a sale belongs to, including co-sell splits. Your billing system, usually Marketron, supplies the conversion, what was booked and billed. Siren credits each conversion to the owner Influence FM names and calculates commission under your real plan: tiers, splits, overrides, and clawbacks included. ### What Siren does on Influence FM - Attribute orders to the account owner: Credit each billed order to the rep Influence FM says owns the account, co-sell splits included, instead of relying on a raw salesperson field. (Siren Cloud Full-Service (hosted and managed)) - Read what was billed: Take booked and billed orders from your traffic system, usually Marketron, as the conversion events that earn commission. (Siren Cloud Full-Service (hosted and managed)) - Reconcile clawbacks: Reverse credit before payout when the billing system reports an order unpaid, and reinstate it on your managed plan if the advertiser later pays. A made-good that re-airs is revenue-neutral, so it does not reverse the credit. (Siren Cloud Full-Service (hosted and managed)) - Produce reconciled statements: Calculate each rep's real owed amount and hand off statements, approvals, and payouts. (Siren Cloud Full-Service (hosted and managed)) ### Events Siren can track - Order booked and billed - New-business vs renewal - Collections and cash receipts - Non-payment and made-goods - Account ownership and co-sell split (Influence FM) ### Rewards Siren can pay - Flat percentage on billing or collections - Tiered rates and accelerators - New-business vs renewal differentials - Splits and sales-manager overrides - Spiffs, contests, and bonuses - Clawbacks on non-payment, before payout ### Program types you can run on Influence FM - Sales commission program - Tiered or accelerator comp plan - Split and override plan - Spiff and contest program - Renewal and expiring-order bonus ### Edition availability - WordPress plugin (self-hosted): Not available yet. Not applicable to this integration. Running comp on the Influence FM stack is a managed Siren Cloud composition. - Siren Cloud self-serve (coming soon): Not available yet. Reading Influence FM ownership and your billing system is delivered on the managed edition, where our team builds and runs the connection. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud reads account ownership from Influence FM and conversions from your billing system, then runs the commission program for you. Built and run by our team during setup. ### What you'll need - Influence FM as your rep CRM, where account ownership and co-sell splits live. - The traffic or billing system that records what was sold, usually Marketron. - A Siren Cloud plan, hosted and set up by our team around your commission plan. - No development work on your side. You supply the comp rules; we read ownership from Influence FM and conversions from billing. ### Good to know - Influence FM is the attribution layer: it holds account ownership, who owns the advertiser, who co-sells it, who works the expiring order. That is what tells Siren whose a billed sale is. - Your billing system, usually Marketron, is the conversion layer: what was booked, billed, and collected. Siren credits each conversion to the owner Influence FM names. - Siren reads account ownership from Influence FM during setup, built and run by our team. - Influence FM stays your CRM. Siren adds the comp engine; it does not replace activity tracking or account management. ### Frequently Asked Questions **Why is Influence FM part of the Siren stack and not just a roster?** Because it is the attribution layer. In an affiliate program, attribution comes from tracking links and coupons. In a radio sales org, attribution comes from the CRM: which rep owns the advertiser, who co-sold it, who owns the expiring order. Influence FM holds that, and it is exactly what Siren needs to credit a Marketron sale to the right rep. The billing system says what sold; Influence FM says whose it is. **Does Influence FM calculate commissions?** No. Influence FM is a rep CRM built for activity and account management: call tracking, appointment reminders, expiring-order alerts, and Smart Rate yield. Compensation is not what it is for. Siren adds the comp layer, using Influence FM ownership as attribution and your billing system as the conversion. **How does Siren know which rep gets credit for an order?** From Influence FM account ownership, not a raw salesperson field on the order. The CRM is where ownership, co-sell splits, and reassignments actually live, so it is the cleaner source of attribution. Siren reads that ownership and credits each billed order to the rep who owns the account, which is the question reps argue about most. **How does Siren connect to Influence FM?** Siren reads account ownership from Influence FM, and our team builds and runs that connection during setup. That ownership is the attribution Siren credits sales against, the part that decides which rep a billed order belongs to. **Can Siren handle tiers, splits, and clawbacks for our reps?** Yes. Those are first-class in Siren's comp rules and applied automatically on every order, instead of reconciled in a spreadsheet at month end. Co-sell splits in particular come straight from Influence FM ownership rather than a manual list. **If our billing system is not Marketron, does this still work?** Yes. Influence FM stays the attribution layer either way; what changes is the conversion source Siren reads. Marketron is the common case in radio, so the pages describe it, but a different traffic or billing system is a matter of pointing the conversion side somewhere else. **If Smart Rate lowers a rate in a soft daypart, is the rep commissioned on the lower amount?** Yes. Siren commissions the real billed amount, whichever way yield management moved it. That cuts both ways: a rep earns more when Smart Rate lands a higher rate and less when it lands a lower one, because the commission follows what actually billed rather than a list price. It is the honest number either way. **Will Siren replace Influence FM?** No. Influence FM keeps your reps accountable and owns the account relationships day to day. Siren sits on the stack as the compensation engine, using that ownership to pay the right rep. The two solve different problems for the same team. ## LearnDash integration Source: https://www.sirenaffiliates.com/integrations/learndash Siren integrates natively with LearnDash to power affiliate programs, instructor royalties, and completion-based revenue sharing. Track course sales, course and lesson completions, and pay on outcomes. > Can you run an affiliate or royalty program on LearnDash? Yes. Siren listens to LearnDash transaction and completion events directly, so it works whether you sell through LearnDash checkout or WooCommerce. Run affiliate programs, instructor royalties, and completion-based revenue sharing on the same WordPress site. Free to start on Lite. ### What Siren does on LearnDash - Track LearnDash transactions: Read course purchases through LearnDash checkout or WooCommerce as conversion events. (all editions) - Track course and lesson completions: Fire rewards when a student finishes a course or an individual lesson. (all editions) - Completion-based revenue sharing: Pool revenue each period and split it among instructors by engagement score. (all editions) - Pay real money via Stripe Connect: Partners self-onboard Stripe Express and get paid automatically on the Plus tier. (all editions) ### Events Siren can track - Course purchase (LearnDash or WooCommerce) - Course completion - Lesson completion - Refund or clawback reversal (WooCommerce-routed sales) - Free enrollments with no payment (not supported) ### Rewards Siren can pay - Flat or percentage commission - Per-course rates - Instructor royalties - Completion-based revenue share - Recurring on renewals (WooCommerce-routed sales) ### Program types you can run on LearnDash - Affiliate program - Instructor royalty program - Completion-based revenue share - Student referral program - Multi-tier partner program (Pro) ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as LearnDash and own your data. Tracks transactions and completions out of the box. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run LearnDash with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Prefer it managed? Siren Cloud runs the same engine for your LearnDash site, hosted and supported by our team. ### What you'll need - A WordPress site with LearnDash active. - Selling through WooCommerce too? Siren tracks both checkouts. - For instructor royalties and revenue share: the Essentials tier. - For automatic real-money payouts: Plus (Stripe Connect). ### Good to know - Course and lesson completion tracking works regardless of how the student originally enrolled. - Completion-based revenue sharing uses the Distributor feature on Essentials and up. ### Pre-built recipes for LearnDash - Course Affiliate Program (Affiliate program), Lite · Free: Percentage commission on every referred LearnDash course sale. Install from https://www.sirenaffiliates.com/recipes/course-affiliate-program - Instructor Royalties (Royalty program), Essentials: Creators earn a percentage when their assigned courses sell. Install from https://www.sirenaffiliates.com/recipes/course-creator-royalty-program - Revenue Share (Revenue share), Essentials: Split a pool of revenue among instructors by engagement. Install from https://www.sirenaffiliates.com/recipes/instructor-revenue-share ### Program guides for LearnDash - Run an affiliate program on LearnDash: https://www.sirenaffiliates.com/integrations/learndash/affiliate-program - Run a royalty program on LearnDash: https://www.sirenaffiliates.com/integrations/learndash/royalty-program ### Frequently Asked Questions **Does Siren work with LearnDash's built-in payments or only WooCommerce?** Siren integrates natively with LearnDash. It listens to LearnDash transaction events directly, so it works whether you sell courses through LearnDash's own checkout or through WooCommerce. Course and lesson completion tracking works regardless of how the student originally enrolled. **What LearnDash events does Siren track?** Siren reads transactions when a student purchases a course, course completions when a student finishes an entire course, and lesson completions when a student finishes an individual lesson. You can use any combination of these three event types to power your affiliate, royalty, and revenue share programs. **Can I pay different commission rates for different courses?** Yes. You can set per-product commission rules, so a $997 certification course can carry a different rate than a $29 introductory course. This lets you align payouts with the actual margins on each offer. **Can I run an affiliate program and an instructor royalty program at the same time?** Yes. Siren lets you create independent programs that evaluate the same transaction separately. When a student buys a course, the affiliate who referred them earns their commission and the instructor who created the course earns their royalty. Both fire on the same sale because they reward different people for different contributions. **How does the completion-based revenue sharing work?** Siren's distributor feature pools a percentage of your revenue each month and splits it among instructors proportionally based on engagement scores. You configure which events count (course completions, lesson completions) and how much each is worth. An instructor whose courses generated 40% of all tracked completions receives 40% of the pool. **Do I need a developer to set this up?** If you are comfortable installing WordPress plugins and configuring LearnDash, you can handle the setup. Siren uses a point-and-click interface for creating programs, setting commission rates, and assigning courses to instructors. For complex multi-program setups, we are available in support. **Can I migrate from another affiliate plugin?** In most cases, yes. You can import your existing partner list and begin tracking new referrals through Siren going forward. Historical commission data migration depends on what your current tool can export. We will help you through the process in support. **What happens when a student gets a refund?** You control commission approval timing. Commissions start in a pending state, so you can hold them until your refund window closes and only then mark them payable. LearnDash does not expose a refund event to Siren, so refunds are handled through this manual review window rather than an automatic clawback. If you sell the same courses through WooCommerce, refunds on those orders are tracked automatically. ## LifterLMS integration Source: https://www.sirenaffiliates.com/integrations/lifterlms Siren is a LifterLMS affiliate plugin for affiliate, referral, and revenue-share programs around your courses and memberships. Reward instructors, pay on renewals, and track completions. Free to start. > What programs can you run on LifterLMS? Affiliate, referral, instructor revenue-share, and royalty programs. Siren is a WordPress plugin that runs natively inside LifterLMS, reading enrollment, transaction, and completion events so partners and instructors are credited the moment a course sells. Start free on Lite. ### What Siren does on LifterLMS - Track course and membership sales: Read LifterLMS transactions, enrollments, and membership renewals as conversion events. (all editions) - Reward lesson and course completions: Fire rewards on course or lesson completion, not only on the sale. (all editions) - Pay instructor revenue share: Pool a share of revenue each period and split it among instructors by engagement. (all editions) - Pay real money via Stripe Connect: Partners self-onboard Stripe Express and get paid automatically on the Plus tier. (all editions) ### Events Siren can track - Course or membership purchase - Membership renewal - Course completion - Lesson completion - Lead capture (Gravity Forms) - Free enrollments with no payment (not supported) ### Rewards Siren can pay - Flat or percentage commission - Per-course or per-membership rates - Recurring on renewals - Instructor revenue share - Completion-based rewards ### Program types you can run on LifterLMS - Affiliate program - Instructor revenue share - Course creator royalty - Student referral program - Multi-tier partner program (Pro) ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as LifterLMS and own your data. Tracks course events out of the box. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run LifterLMS with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Prefer it managed? Siren Cloud runs the same engine for your LifterLMS site, hosted and supported by our team. ### What you'll need - A WordPress site with LifterLMS active. - For renewal and membership rewards: the Essentials tier. - For instructor revenue share and the Collaborator Portal: Essentials and up. - For automatic real-money payouts: Plus (Stripe Connect). ### Good to know - Completion-based rewards and instructor revenue share use the Distributor feature on Essentials and up. - Multi-tier and cascading commissions are a Pro-tier feature. ### Pre-built recipes for LifterLMS - Course Affiliate Program (Affiliate program), Lite · Free: Percentage commission on every referred course sale. Install from https://www.sirenaffiliates.com/recipes/course-affiliate-program - Instructor Revenue Share (Revenue share), Essentials: Split a pool of revenue among instructors by engagement. Install from https://www.sirenaffiliates.com/recipes/instructor-revenue-share - Creator Royalties (Royalty program), Essentials: Creators earn a percentage when their assigned courses sell. Install from https://www.sirenaffiliates.com/recipes/course-creator-royalty-program - Course Platform Starter (Marketplace), Essentials: A ready-made set of programs for a course marketplace. Install from https://www.sirenaffiliates.com/recipes/online-course-platform-starter ### Program guides for LifterLMS - Run an affiliate program on LifterLMS: https://www.sirenaffiliates.com/integrations/lifterlms/affiliate-program - Run revenue share on LifterLMS: https://www.sirenaffiliates.com/integrations/lifterlms/revenue-share ### Frequently Asked Questions **Does Siren work with my existing LifterLMS site?** Yes. Siren is designed to connect to an existing LifterLMS site so you can start tracking affiliates, referrers, and partners without rebuilding your courses. **Do I need a developer to set this up?** Most setups do not. If you are comfortable installing WordPress plugins and configuring LifterLMS, you can get Siren running. If your site is heavily customized, we are happy to advise. **Can I pay different rates for different courses or memberships?** Yes. You can create per-product commission rules so high-ticket offers, memberships, and lower-priced courses can each have their own structure. **Can I run both an affiliate program and instructor revenue share?** Yes. Siren lets you define different programs so you can pay affiliates for referrals and pay instructors for their role in creating and maintaining content. **Does Siren support recurring commissions for memberships?** If your LifterLMS setup sells recurring memberships, you can choose whether partners earn on the first payment only or on a defined set of renewals. **Can I migrate from another LifterLMS affiliate plugin?** In many cases, yes. You can bring over your partner list and start tracking new referrals through Siren going forward. Historical data import depends on what your current tool can export. We will help you in support. **What happens if someone cancels or gets a refund?** You control when commissions are approved. If a student cancels or gets a refund within your policy window, you can automatically or manually prevent that commission from being paid. **Is Siren only for affiliates?** No. Siren is designed for affiliates, referrers, instructors, ambassadors, and JV partners. It is a full incentive engine for your LifterLMS site. ## Marketron integration Source: https://www.sirenaffiliates.com/integrations/marketron Marketron's commission report applies a flat rate per rep. Siren Cloud reads Marketron billing and AR through its Integration Suite, or a data feed we set up, and calculates what reps are owed: tiers, splits, overrides, and clawbacks. > Can you run sales commissions on top of Marketron? Yes, with Siren Cloud. Marketron is the system of record for orders, spots, invoices, and AR, but its commission report is a flat rate table. Siren reads Marketron's billing and collection events through its Integration Suite and calculates what each rep is owed under your plan as written, line by line so the math is auditable: tiers, splits, overrides, spiffs, and clawbacks included. ### What Siren does on Marketron - Read billing and collections from Marketron: Read orders, spots, invoices, and cash receipts through the Integration Suite REST APIs as the source of comp events. (Siren Cloud Full-Service (hosted and managed)) - Reconcile against AR and clawbacks: Reverse a rep's credit before payout when the billing system reports an order unpaid, and reinstate it on your managed plan if the advertiser later pays. A made-good spot that re-airs is revenue-neutral, so it does not reverse the credit. (Siren Cloud Full-Service (hosted and managed)) - Map reps and advertiser accounts: Match Marketron salespeople and accounts to Siren collaborators and comp plans. (Siren Cloud Full-Service (hosted and managed)) - Give every rep their own statement: Each rep sees every order, rate, split, and clawback behind their number, with the Marketron event for each line, so disputes are settled against the record. (Siren Cloud Full-Service (hosted and managed)) - Produce reconciled payout statements: Calculate each rep's real owed amount and hand off statements, approvals, and payouts. (Siren Cloud Full-Service (hosted and managed)) ### Events Siren can track - Order or spot booked - Invoice issued (billing) - Payment collected (cash receipt) - Non-payment and AR aging - Spot bumped or made-good ### Rewards Siren can pay - Flat percentage on billing or collections - Tiered rates and accelerators - New-business vs renewal differentials - Splits and sales-manager overrides - Spiffs, contests, and bonuses - Referral fees to non-employees ### Program types you can run on Marketron - Sales commission program - Tiered or accelerator comp plan - Split and override plan - Spiff and contest program - Local-business referral program ### Edition availability - WordPress plugin (self-hosted): Not available yet. Not applicable to this integration. Reading Marketron billing is a managed Siren Cloud composition. - Siren Cloud self-serve (coming soon): Not available yet. Composing onto a Marketron system is delivered on the managed edition, where our team builds and runs the connection. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud composes onto Marketron through its Integration Suite REST APIs, reads your billing and collection events, and runs the commission program for you. We build and run the composition as part of setup. ### What you'll need - A Marketron system you can pull from. The cloud products expose order, billing, and AR data through the Marketron Integration Suite; on editions without a REST API, we set up a data feed during the build. - A Siren Cloud plan, hosted and set up by our team around your commission plan. - Your real comp rules: tiers, new-business vs renewal rates, splits, overrides, spiffs, and clawback terms. - No development work on your side. You supply the comp rules and rep roster; we build the connection onto Marketron's APIs. ### Good to know - Marketron stays your system of record. Siren reads its events and calculates comp; it does not move money or write back into Marketron. - Siren reads Marketron through its Integration Suite APIs on the cloud products, or a data feed we set up on editions without a REST API, and our team builds and runs that connection during setup. - Every number traces to a Marketron event, so reps see their own statements and a disputed line can be reviewed and adjusted before payout. - The thing Siren replaces is the commission spreadsheet stations keep on top of Marketron's flat report, not Marketron itself. - Pairs with Influence FM, the CRM that holds account ownership, so Siren can credit each Marketron order to the rep who actually owns the account, co-sell splits included. ### Program guides for Marketron - Run account-executive commissions on Marketron: https://www.sirenaffiliates.com/integrations/marketron/ae-commission - Get rep commission right on agency business: https://www.sirenaffiliates.com/integrations/marketron/agency-commission - Pay local referral fees on top of Marketron: https://www.sirenaffiliates.com/integrations/marketron/local-referral - Pay new business and renewals differently on Marketron: https://www.sirenaffiliates.com/integrations/marketron/new-business-vs-renewal - Run rep splits and manager overrides on Marketron: https://www.sirenaffiliates.com/integrations/marketron/rep-splits-and-override - Run a sales contest on top of Marketron: https://www.sirenaffiliates.com/integrations/marketron/sales-contest ### Frequently Asked Questions **Does Marketron calculate sales commissions?** It produces a commission report, but it is a flat rate table tied to billing or collections. It cannot handle tiers, accelerators, new-business versus renewal rates, splits, manager overrides, spiffs, or clawbacks. The moment your plan includes any of those, the report gets exported to Excel and adjusted by hand. Siren is the engine that does that math automatically. **How does Siren connect to Marketron?** Marketron's Integration Suite exposes order, spot, AR, and account data over REST APIs, and Siren Cloud composes onto those. Our team builds and runs the connection as part of setup. **Do you pay on billing or on collections?** Either. Siren reads both billing and cash-receipt events from Marketron, so you can pay reps when an order is invoiced or when the advertiser actually pays, and reverse credit before payout when an invoice goes unpaid. A made-good that re-airs does not reduce billing, so it does not reverse the credit. **Can Siren handle splits and sales-manager overrides?** Yes. Splits between reps and overrides to a sales manager are first-class in Siren's comp rules, applied automatically on every order rather than reconciled in a spreadsheet. **Can we pay a local business that refers an advertiser?** Yes. Siren is not limited to employees. Referral fees to local businesses, agency arrangements, and station promo or loyalty programs run on the same engine as your rep commissions. **Which Marketron product and API does Siren read, and what if I am on an older edition?** Siren reads order, billing, and AR data through the Marketron Integration Suite, the connector-and-API layer on the cloud products. On an older or on-premises edition without a REST API, we set up a data feed instead. Our team builds the connection to your specific Marketron during setup. **How current is the data when I run payroll on the first?** Siren pulls on a cadence we set with you, typically a nightly sync with a settling window so late billing adjustments are captured before a period closes. If an invoice is corrected after you have already paid commission on it, Siren trues the difference up in the next period rather than silently restating a closed one. **A rep disputes their number. How do I review and adjust it before payout?** Every line on a statement traces to a Marketron event, so a dispute is settled against the record, not someone's memory. You can review a run, adjust a line, and approve before payout, and the rep sees the adjusted statement with the reason. The audit trail is the point: it is what makes the number defensible to the rep and to your auditor. **How much of my team's time does setup actually take, and what do you need from me?** The build is on us; what we need from you is access to your Marketron data and your comp rules written down precisely, plus your rep roster and the Marketron salesperson codes to map against. That is real sales-ops time on your side, usually measured in a handful of sessions, not zero. We would rather be honest about that than tell you it is effortless and surprise you later. **Siren is reading our AR and rep pay. Where does that data live and is the connection read-only?** The connection is read-only. Siren reads Marketron data to calculate statements and never writes back or moves money. Where the data is hosted, how credentials are stored, and our security posture are part of the setup conversation, because for a system touching billing and AR those are the right questions to ask before you sign anything. **What does this cost?** Siren Cloud is custom-priced for radio: a one-time setup to build the Marketron connection and your comp model, plus an ongoing managed subscription. There is no per-order fee skimmed from your billing. The number is shaped by how many stations and how complex your plan is, so tell us your setup and we will scope real figures rather than quote a sticker price that fits nobody. **Will this replace Marketron?** No. Marketron stays your traffic, billing, and AR system of record. Siren sits on top as the incentive layer, reading Marketron events and calculating what your people are owed. What it replaces is the monthly commission spreadsheet. ## NetSuite integration Source: https://www.sirenaffiliates.com/integrations/netsuite Siren Cloud connects to NetSuite, reads sales orders, invoices, and credit memos, and runs rep commissions, dealer rebates, and partner revenue share on net revenue. Reconciled, audit-ready, hosted, and managed. > Can you run sales commissions and dealer rebates on NetSuite? Yes, with Siren Cloud. NetSuite stays the system of record for your transactions, and Siren connects over the API to read sales orders, invoices, credit memos, and the records your comp and rebate plans depend on. From there it calculates rep commissions, dealer rebates, and partner revenue share under your rules, nets out returns and credits, and produces a statement your finance team can pay and trace line by line. For dealers and outside partners, Siren can also disburse the payout directly. Built for businesses that run finance on NetSuite. ### What Siren does on NetSuite - Read NetSuite sales transactions: Use sales orders, invoices, and amounts as the source of truth for what to pay and to whom. (Siren Cloud Full-Service (hosted and managed)) - Reconcile sell-through and returns: Net out credit memos, returns, and chargebacks, and reconcile the sell-through feeds your rebates depend on, so payouts reward net revenue rather than gross. (Siren Cloud Full-Service (hosted and managed)) - Calculate commission and rebate plans: Model tiers, dealer rebates, splits, and thresholds, plus period-based programs that allocate a growth or volume rebate pool across dealers by their share of sell-through. Set the rules once and Siren applies them to every transaction and every period automatically. (Siren Cloud Full-Service (hosted and managed)) - Produce an audit-ready statement: Every payout traces back to the NetSuite transactions behind it and the rule that was applied, so finance can tie out the number and answer an auditor. (Siren Cloud Full-Service (hosted and managed)) - Pay outside partners directly: Finance pays internal reps from the reconciled statement through payroll, AP, PayPal, or ACH. For dealers, distributors, and outside partners, Siren can also disburse the payout itself through Stripe Connect. (Siren Cloud Full-Service (hosted and managed)) ### Events Siren can track - Sales order booked - Invoice issued or paid - Credit memo or return - Chargeback or commission clawback - Sell-through feed you supply, such as EDI 867 - New dealer or partner account - Quote with no order (not supported) ### Rewards Siren can pay - Percentage of order or invoice - Tiered dealer rebate - Growth or sell-through rebate - Rep commission with splits - Partner revenue share - Volume or growth bonus ### Program types you can run on NetSuite - Sales commission program - Dealer and channel rebates - Partner revenue share - Distributor incentive program - Rep SPIFF and bonus program ### Edition availability - WordPress plugin (self-hosted): Not available yet. Not available. The WordPress plugin runs on your own site and tracks WordPress sales. NetSuite is a hosted ERP, so it connects through Siren Cloud. - Siren Cloud self-serve (coming soon): Not available yet. The hosted, sign-up-yourself edition. NetSuite is connected and run for you on our managed Cloud edition. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud connects to your NetSuite account over its API and runs the program for you, hosted, set up, and supported. ### What you'll need - A NetSuite account you can connect, with read access to sales orders and invoices. - A Siren Cloud plan, hosted and set up by our team around your comp and rebate model. - Your plan rules: rates, tiers, dealer rebates, splits, and return handling. - Sell-through feeds (such as EDI 867) where your rebates depend on them. ### Good to know - Siren reads NetSuite data to calculate payouts. Finance pays from the reconciled statement through payroll, AP, PayPal, or ACH, and for dealers and outside partners Siren can disburse payouts directly through Stripe Connect when you want it to. - Finance can open any payout and see the booked transactions it came from and the plan rule that set it, so the number stands up in review. - Payouts can reward net revenue, with credits, returns, and chargebacks reversing commission automatically. - We build the program with you. There is no engineering lift beyond giving Siren read access to your NetSuite account. - Sell-through programs are supported where you can supply the feeds, including EDI 867 and flat-file exports. - The same model fits other systems of record. If you run a different ERP, Salesforce, or custom transaction data, Siren can read those instead. ### Pre-built recipes for NetSuite - Sales Commissions (Sales commissions), Cloud: Pay reps from NetSuite sales orders and invoices under your comp plan. Install from https://www.sirenaffiliates.com/recipes/sales-team-commission-program - Dealer Rebates (Rebate program), Cloud: Calculate tiered rebates from sell-through and reconcile against returns. Install from https://www.sirenaffiliates.com/recipes/channel-partner-program - Partner Revenue Share (Revenue share), Cloud: Split revenue with channel partners straight from booked transactions. Install from https://www.sirenaffiliates.com/recipes/business-partner-revenue-share ### Frequently Asked Questions **We already have NetSuite's Incentive Compensation module. Why would we add Siren?** If the native module fits your plan, use it. Teams come to Siren when their rules go beyond what it models cleanly, like commission on margin or cost instead of revenue, complex splits and team deals, multi-subsidiary credit, or dealer rebates on sell-through. Those are the cases where finance ends up back in a spreadsheet. Siren reads the same NetSuite transactions and runs the plan you actually have, then hands finance a reconciled statement. **We run NetSuite OneWorld across multiple subsidiaries and currencies. Can Siren handle that?** Yes. Siren reads transactions across the subsidiaries you connect and applies credit the way your plan assigns it, including deals that span entities. Amounts come through in the currency NetSuite recorded, and your plan decides how they roll up, so a rep or partner working across subsidiaries lands on one reconciled statement. **How does Siren handle returns, credit memos, and clawbacks so we are not paying on revenue we gave back?** Siren reads your credit memos, returns, and chargebacks from NetSuite and reverses the commission or rebate tied to them. Payouts are calculated on net revenue, so a return in this period trues up the statement automatically rather than becoming a clawback you chase a rep or dealer for later. **Is the output auditable? Can my team tie out a payout to the source data?** Yes. Every line on a Siren statement traces back to the NetSuite transactions behind it and the rule that produced it. Your finance team can see how each number was derived, answer a dispute, and show an auditor the work. **How much work is this for my team? Do we need a developer or a NetSuite admin to build it?** We build it with you. You give Siren read access to your NetSuite account and tell us how your comp and rebate plans work, and our team models the rules and runs the program hosted. There is no engineering lift beyond connecting the account, and nothing for your NetSuite admin to maintain. **How is this different from CaptivateIQ, Spiff, or a rebate platform like Enable?** Those are self-serve platforms you license per seat and administer yourself. Siren Cloud is a managed calculation layer we build and run with you. It reads the transactions NetSuite already records and produces reconciled commission, rebate, and revenue-share statements your finance team pays from. If you want a platform to staff and operate in-house, those tools fit. If you want the calculation handled and an audit-ready statement each period, that is what Siren does. **How does this fit our month-end close?** Siren produces a reconciled statement each period on the cadence your close runs, calculated from the NetSuite transactions booked in that period and netted against returns and credits. Finance pays from the statement and ties it back to the ledger. **What does a NetSuite commission program with Siren cost?** Siren Cloud is custom-priced: a one-time setup to connect NetSuite and model your comp and rebate plans, plus an ongoing managed subscription. Tell us how your plans work and we will scope real numbers. ## Ninja Forms integration Source: https://www.sirenaffiliates.com/integrations/ninja-forms Siren turns Ninja Forms into a partner-ready engine for signups, paid leads, and form-based sales. Tie key submissions to the partner who sent them and pay for the value they create. > Can you run a pay-per-lead or partner program with Ninja Forms? Yes. Siren connects to the Ninja Forms you already have and decides what each submission means: a partner signup, a paid lead, or a sale. Key submissions get tied to the partner who drove them, so you can pay per qualified lead or per form-based sale. Start free on Lite. ### What Siren does on Ninja Forms - Track form submissions as events: Read connected Ninja Forms submissions as leads, signups, or sales. (all editions) - Capture partner signups: Add a Siren action to a form so it becomes partner onboarding, creating affiliates on submit. (all editions) - Track form payments: When someone pays through a connected form, record the order total and attribute it. (all editions) - Pay per qualified lead: Keep a running count of qualified leads a partner sends and pay against it. (all editions) ### Events Siren can track - Form submission (lead or application) - Qualified paid lead - Form payment (Stripe or PayPal via Ninja Forms) - Partner signup form - Submissions you have not marked qualified (not supported) ### Rewards Siren can pay - Flat bounty per qualified lead - Percentage on form-based sales - Signup and application bonuses - Tiered by lead volume - Manual review before payout ### Program types you can run on Ninja Forms - Pay-per-lead program - Partner or affiliate signup - Lead-gen partner program - Form-based sales commissions - Ambassador program ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as Ninja Forms and own your data. Connect forms in a few clicks. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run Ninja Forms with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Prefer it managed? Siren Cloud runs the same engine for your Ninja Forms site, hosted and supported by our team. ### What you'll need - A WordPress site with Ninja Forms active. - The forms you want to connect, plus a rule for what counts as a qualified lead. - For automatic payouts: Essentials and up. Real-money payouts on Plus (Stripe Connect). - Your CRM and email tools stay where they are. Siren just feeds them richer data. ### Good to know - Siren does not replace your CRM or email automations. It makes sure they receive richer data, like who referred a contact. - Siren can sit behind multiple sources at once, so partners get one record across Ninja Forms, WooCommerce, and more. ### Pre-built recipes for Ninja Forms - Cost Per Lead (Lead-gen program), Essentials: Pay a flat bounty for each qualified lead a partner sends. Install from https://www.sirenaffiliates.com/recipes/cost-per-lead-campaign - Pay-Per-Lead Affiliate (Affiliate program), Essentials: Reward affiliates for qualified leads, not just sales. Install from https://www.sirenaffiliates.com/recipes/pay-per-lead-affiliate-program - B2B Referral (Referral program), Essentials: Reward partners who refer qualified business inquiries. Install from https://www.sirenaffiliates.com/recipes/b2b-referral-program ### Program guides for Ninja Forms - Run an affiliate program on Ninja Forms: https://www.sirenaffiliates.com/integrations/ninja-forms/affiliate-program - Run a lead-gen program on Ninja Forms: https://www.sirenaffiliates.com/integrations/ninja-forms/lead-gen-program ### Frequently Asked Questions **Does this work with the Ninja Forms setup I already have?** Yes. You add a Siren action to specific forms and decide what they do: create affiliates, leads, or sales. Most of the time you are wiring in Siren, not redesigning your forms. **Do I need a developer to use this?** If you are already comfortable building forms and adding actions in Ninja Forms, you can usually handle this yourself. If your site is heavily customized or mission-critical, a developer can help tighten things up. **Will this replace my CRM or email automations?** No. Your CRM and email tools stay where all the nurturing happens. Siren simply makes sure those tools receive richer data, like who referred a contact and which partner program they are in. **Can I run pay-per-lead programs with this?** Yes. You can mark forms as paid lead sources, give partners links to those forms, and let Siren keep a running count of qualified leads they send. You decide what counts as qualified, and you pay against that, not guesses. **We take payment through Ninja Forms with Stripe or PayPal. Can Siren still track commissions?** Yes. When someone pays through a connected form, Siren records the order total, ties it to the right affiliate, and includes it in your partner reporting and payouts. **What if I use WooCommerce or another platform and Ninja Forms?** That is fine. Siren can sit behind multiple sources at once, so partners get one consistent record of the leads and sales they drive, no matter which front-end you used. **What happens if a lead is junk or a customer asks for a refund?** You are still in control. Leads and form-based sales can be reviewed or adjusted before you pay out, so a junk lead or a refunded order does not have to earn a commission. Siren gives you structure and the final say. It does not force you to pay for bad fits. ## North Commerce integration Source: https://www.sirenaffiliates.com/integrations/north-commerce Siren is a North Commerce affiliate plugin for affiliate, referral, and revenue-share programs around your products, subscriptions, and memberships. Track every order and reward partners on your own site. > Can you run an affiliate program on North Commerce? Yes. Siren is a WordPress plugin that runs natively alongside North Commerce, reading order events so affiliate, referral, and revenue-share programs are tracked and paid the moment a sale completes. Siren is built for modern, high-AOV offers and creator-driven launches. Start free on Lite. ### What Siren does on North Commerce - Track orders and refunds: Read North Commerce orders and refunds as conversion events. (all editions) - Attribute by coupon code: Tie a coupon to a collaborator so every checkout that uses it is credited automatically. (all editions) - Pay as store credit: Issue automatic store credit that partners redeem at checkout. (all editions) - Pay real money via Stripe Connect: Partners self-onboard Stripe Express and get paid automatically on the Plus tier. (all editions) ### Events Siren can track - New order or sale - Specific-product sale - Refunds and clawbacks - Sales made off your WordPress site (not supported) ### Rewards Siren can pay - Flat or percentage commission - Per-product rates - Contest bonuses - Tiered by volume ### Program types you can run on North Commerce - Affiliate program - Customer referral program - Influencer or creator program - Distributor or reseller network - Multi-tier partner program (Pro) ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as North Commerce and own your data. Tracks orders out of the box. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run North Commerce with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Prefer it managed? Siren Cloud runs the same engine for your North Commerce store, hosted and supported by our team. ### What you'll need - A WordPress site with North Commerce active. - For coupon-code tracking and the Collaborator Portal: the Essentials tier. - For automatic real-money payouts: Plus (Stripe Connect). - No API keys and no developer setup. It is the same WordPress site. ### Good to know - The self-hosted plugin tracks events on your WordPress site. To attribute sales off WordPress too, run it on Siren Cloud. - Multi-tier and cascading commissions are a Pro-tier feature. ### Pre-built recipes for North Commerce - Affiliate Program (Affiliate program), Lite · Free: Percentage commission on every referred North Commerce sale. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program - Refer a Friend (Referral program), Essentials: Reward existing customers who refer new buyers. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program - Influencer Coupons (Influencer program), Essentials: Track influencer commissions through unique coupon codes. Install from https://www.sirenaffiliates.com/recipes/coupon-based-influencer-program ### Program guides for North Commerce - Run an affiliate program on North Commerce: https://www.sirenaffiliates.com/integrations/north-commerce/affiliate-program - Run a referral program on North Commerce: https://www.sirenaffiliates.com/integrations/north-commerce/referral-program ### Frequently Asked Questions **Does Siren work with my existing North Commerce store?** Yes. Siren installs as a WordPress plugin on the same site as North Commerce and reads your existing orders natively. You do not rebuild your catalog or sign up for a separate platform. Define your programs and Siren starts tracking affiliate and referral activity on the orders already flowing through your store. **Do I need a developer to set this up?** Most store owners do not. If you are comfortable installing WordPress plugins and configuring North Commerce, you can get Siren online. For heavily customized stacks, we are happy to advise. **Can I pay different rates for different products?** Yes. You can set commission rules by product or category, so flagship and high-AOV offers can carry a different rate than entry-level products. **Can I track partners by coupon code instead of links?** Yes. Assign a coupon to a collaborator and every checkout that uses it is credited to them automatically. This is useful for influencers who would rather share a code. **What happens if someone cancels or gets a refund?** You control when commissions are approved. If an order is refunded within your policy window, you can reject the commission so it is never paid out. **Is Siren only for affiliates?** No. Siren is built for affiliates, referrers, creators, ambassadors, and joint-venture partners. It is a full incentive engine for your North Commerce store. ## Salesforce integration Source: https://www.sirenaffiliates.com/integrations/salesforce Siren Cloud connects to Salesforce, reads closed-won opportunities, and runs sales commissions, channel partner payouts, and referral rewards on the deals your CRM already records. Reconciled, hosted, and managed. > Can you run sales commissions and partner payouts on Salesforce? Yes, with Siren Cloud. Salesforce stays the system of record for your pipeline. Siren connects to it over the API, reads closed-won opportunities and the deal fields your comp plan depends on, and calculates rep commissions, channel-partner payouts, and referral rewards under your plan. It reads only, so nothing changes inside Salesforce, and it produces a reconciled statement instead of moving money. Best fit for teams that close in Salesforce, pay more than just internal reps, and want incentive math out of spreadsheets without adopting and administering a full commission tool themselves. ### What Siren does on Salesforce - Read closed-won opportunities: Use Salesforce deals, amounts, and owners as the source of truth for what to pay and to whom. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Attribute by partner or referrer: Match an opportunity to a channel partner, reseller, or referring customer using your CRM fields. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Run any comp model on one engine: Tiers, splits, accelerators, recurring renewal share, and performance-weighted pools all run on the same engine, so reps, channel partners, and referrers can be paid by different rules from the same deals. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Pay partners directly or by statement: Internal reps are paid from a reconciled statement through payroll or AP. For channel partners, resellers, and referrers, Siren can disburse the payout directly through Stripe Connect. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) ### Events Siren can track - Opportunity closed-won - Deal amount, stage, or split change - Renewal or expansion opportunity - Clawback, refund, or downgrade on a closed deal - New partner or reseller account - Open pipeline with no close (not supported) ### Rewards Siren can pay - Percentage of deal value - Higher rate for a tier of top performers - Bonus rate once a partner passes a volume threshold you set - Split or pool across multiple reps and partners - Flat bonus or SPIFF per closed deal - Recurring share of renewals and expansion ### Program types you can run on Salesforce - Sales commission program - Channel and reseller payouts - Partner and referral program - Customer referral program - SPIFF and bonus program ### Edition availability - WordPress plugin (self-hosted): Not available yet. Not available. The WordPress plugin runs on your own site and tracks WordPress sales. Salesforce is a hosted CRM, so it connects through Siren Cloud. - Siren Cloud self-serve (coming soon): Not available yet. The hosted, sign-up-yourself edition. Salesforce is connected and run for you on our managed Cloud edition. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud connects to your Salesforce org over its API and runs the program for you, hosted, set up, and supported. ### What you'll need - A Salesforce org you can connect, with read access to opportunities and the fields your comp plan uses. - A Siren Cloud plan, hosted and set up by our team around your comp model. - Your comp plan rules: rates, tiers, splits, and any clawback terms. - No Salesforce development. We build it with you and handle the connection. ### Good to know - Siren reads Salesforce data to calculate payouts. It produces the reconciled statement finance pays reps from, and for partners and referrers it can also disburse the payout directly through Stripe Connect. - Siren is an incentive layer on top of your system of record, not a replacement for Salesforce and not a self-serve commission tool you administer. We connect, model, and run the program for you. - Attribution keys off the fields you already use, so a partner, reseller, or referrer maps cleanly to the deals they influenced. - Open pipeline is not a billable event. Siren rewards deals that actually close, and reverses credit on clawbacks. - Siren calculates from the fields you actually use. When an opportunity is missing an owner, an amount, or a partner tag, that deal needs fixing in Salesforce before it can be paid, and our team flags it rather than guessing, so you correct the record instead of the math after. - The same model spans more than one system. Alongside Salesforce, Siren Cloud can read billing in Stripe or another system you connect, so one partner record reconciles across the systems they touch into a single statement, rather than each system paying in its own silo. ### Pre-built recipes for Salesforce - Sales Commissions (Sales commissions), Cloud: Pay reps from closed-won Salesforce opportunities under your comp plan. Install from https://www.sirenaffiliates.com/recipes/sales-team-commission-program - Channel Partner Payouts (Partner program), Cloud: Attribute deals to resellers and partners and pay on what they source. Install from https://www.sirenaffiliates.com/recipes/channel-partner-program - Customer Referral (Referral program), Cloud: Reward customers and advocates who refer deals that close. Install from https://www.sirenaffiliates.com/recipes/b2b-referral-program ### Frequently Asked Questions **Why not just calculate commissions in Salesforce reports or formula fields?** You can, up to a point, but it breaks where comp gets real. Formula fields recalculate in real time, so changing a rate can quietly rewrite last quarter's numbers and corrupt your history. There is no built-in audit trail for overrides or adjustments, and anything beyond a flat percentage usually means custom objects and a Salesforce developer. Siren reads from Salesforce and does the calculation outside it, so your history stays fixed. Rate changes apply going forward, one-off adjustments are recorded as adjustments, and every payout stays reconciled to how the plan read at the time. **How is this different from a tool like Spiff, CaptivateIQ, or QuotaPath?** Those are self-serve commission platforms you configure and maintain in-house, built mostly around internal rep comp. Siren Cloud is a managed service. Our team connects Salesforce, models your plan, and runs the calculation for you, and the same model covers rep commissions, channel and reseller payouts, and customer referral rewards together. If you want software your team administers, a dedicated commission tool may fit better. If you want the program built and run for you across reps and partners, that is what Siren Cloud does. **What happens when a deal is refunded, downgraded, or clawed back after it closes?** Siren reverses the credit. Because it reads the current state of the opportunity each period, a refund, downgrade, or reversal backs the related payout out of the statement automatically. Finance ends up paying on the revenue you actually kept, with a record of the adjustment. **How does Siren attribute a Salesforce deal to a partner or rep?** Siren reads the owner, partner, and referral fields on each opportunity and binds the deal to the right person under your rules. The binding lives on the opportunity, not on a click or cookie, so it survives a long B2B cycle, and a first-touch rule can keep credit with the partner who first sourced the account even if someone else touches it later. Splits and team deals are supported, so more than one party can share a single close. **Who sets this up, and do I need a Salesforce admin or developer?** We do, and no. Siren Cloud is hosted and built with our team. We connect to your Salesforce org with read access, model your comp plan with you, and run the program. There is no Salesforce development, no custom objects to maintain, and nothing for your admin to staff each month. **How long does it take to go live?** It depends on how many plans and payees you run, but most setups come down to two things: connecting your org with read access and modeling your comp rules with us. Simple rep-only plans go faster than multi-partner programs with splits and accelerators. We scope the timeline up front when we look at your plan, so you know what to expect before you commit. **Does Siren pay the money, and can reps see their numbers?** It depends who is being paid. For internal reps, Siren produces a reconciled statement your finance team pays through payroll or accounts payable. For partners, resellers, and referrers, Siren Cloud can disburse the payout directly through Stripe Connect, so you are not cutting those checks by hand. Either way each statement breaks out, per person, how the payout was calculated, so when someone asks why a number is what it is, the answer is already there. **What does a Salesforce commission program with Siren cost?** Siren Cloud is custom-priced: a one-time setup to connect Salesforce and model your comp plan, plus an ongoing managed subscription. Tell us how your plan works and we will scope real numbers. ## Shopify integration Source: https://www.sirenaffiliates.com/integrations/shopify Shopify has no native affiliate engine. Siren Cloud connects to your store, reads every order, attributes it to the partner who actually drove it, and pays affiliates, influencers, and ambassadors. Hosted and managed. > Can you run an affiliate program on Shopify? Yes, with Siren Cloud. Shopify has no native affiliate or partner engine, so Siren connects to your store over the Shopify API and webhooks, attributing each order to the partner who referred it and calculating commission automatically. Unlike a single last-click app, Siren can tie a sale to a code, a link, a landing page, or a product, so the right partner gets credit and coupon hunters do not. Best fit for stores that want one program across affiliates, influencers, and customer referrals. ### What Siren does on Shopify - Track Shopify orders as conversions: Reads orders, line items, and discounts as the source of sales, so each one attributes to the partner who referred it. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Attribution that survives the AI checkout: Binds the partner to what the customer actually used, a code, a link, a landing page, or a product line item, instead of a click an AI assistant can absorb. The partner who caused the sale gets paid, even when the click never fires. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Track refunds and cancellations: Reverses commission when a Shopify order is refunded or cancelled, so payouts only reward real revenue. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) - Pay partners directly or by statement: Calculates what each partner is owed with your rates, tiers, and thresholds, then pays them through Stripe Connect or produces a reconciled statement you pay from. (Siren Cloud self-serve (coming soon), Siren Cloud Full-Service (hosted and managed)) ### Events Siren can track - Order created - Order paid - Discount or coupon redeemed - Refund or cancellation - Subscription or recurring order from a Shopify app - Abandoned checkout (not supported) ### Rewards Siren can pay - Percentage of order value - Flat amount per order - Per-product or per-collection rate - Tiered by volume or revenue - Recurring commission on every subscription renewal, where your Shopify subscription app exposes the renewal event - Milestone, contest, or signup bonus - Tiered rates that step up with a partner's volume or revenue - One-time referral bounty ### Program types you can run on Shopify - Affiliate program - Influencer and creator program - Customer referral program - Ambassador program - Revenue share or royalty program - Performance and milestone bonus program - Reseller or distributor partner program ### Edition availability - WordPress plugin (self-hosted): Not available yet. Not available. The WordPress plugin runs on your own site and connects to WooCommerce, not Shopify. Shopify is a hosted, non-WordPress platform, so it connects through Siren Cloud. - Siren Cloud self-serve (coming soon): Not available yet. The hosted, sign-up-yourself edition. Shopify is connected and run for you on our managed Cloud edition. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud connects to your Shopify store over its API and runs the program for you, hosted, set up, and supported. ### What you'll need - A Shopify store you can connect, with read access to orders. - A Siren Cloud plan, hosted and set up by our team around your commission model. - Optionally your email or loyalty tool, so referrals can reach your existing customers. - No theme or checkout engineering. We handle the connection and the setup. ### Good to know - Siren reads Shopify orders to calculate commission, then pays partners directly through Stripe Connect, or produces a reconciled statement you pay from, whichever you prefer. - It is your store and your commission rules. Siren calculates what each partner is owed and can pay them for you through Stripe Connect, or hand you a reconciled statement to pay from. - Selling on both Shopify and WordPress? Siren Cloud can track both and keep one partner record per person across them. - Attribution starts when an order is created or paid. Abandoned checkouts are not billable events. ### Pre-built recipes for Shopify - Store Affiliate (Affiliate program), Cloud: Recruit affiliates and pay a share of every Shopify order they drive. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program - Influencer Program (Influencer program), Cloud: Give creators a link or code and reward the sales it produces. Install from https://www.sirenaffiliates.com/recipes/coupon-based-influencer-program - Customer Referral (Referral program), Cloud: Reward existing customers when a friend they refer buys. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program ### Frequently Asked Questions **Does Shopify have a built-in affiliate program?** No. Shopify runs your store and checkout, but it has no native affiliate, influencer, or referral engine. Stores usually stitch together separate apps for each. Siren Cloud adds one layer that connects to your store over the Shopify API and turns orders into tracked, attributable partner sales. **How is this different from the affiliate apps already on Shopify?** Most Shopify affiliate apps, such as Refersion, UpPromote, or Shopify Collabs, charge a monthly fee plus a share of the revenue your program drives, and they credit the last click, which can pay creators for orders they never influenced. Siren Cloud is a flat managed plan with no per-order skim, and it can attribute a sale by code, link, landing page, or product, not just the last click. It also runs affiliates, influencers, and customer referrals in one program instead of one app per use case. **How do you stop discount-code leakage from inflating creator payouts?** A code that leaks to a coupon site can pay a creator for orders they never influenced. Siren issues a unique code per partner, so a code that spreads still ties back to one partner instead of a shared code anyone can grab. And because Siren can also attribute by link, landing page, or product, credit lands on the partner who really drove the order. **What happens to attribution when people buy through AI assistants like ChatGPT?** AI checkout can absorb the click an affiliate program normally tracks, so a last-click app sees a direct sale and the creator who drove it earns nothing. Siren attributes on what the customer used, a code, a landing page, or a product, not only a click, so the right partner still gets credit when the click never fires. **I already run a program on another app. Can you bring it over?** Yes, that is part of what the managed setup is for. Tell us about your existing partners, their codes, and your commission structure, and our team plans the move so your partners and their rates carry over. You are not starting your program from zero. **How does Siren actually pay my partners?** Two ways, your choice. Siren can pay partners real money directly through Stripe Connect, where each partner self-onboards and gets paid automatically each period. Or it produces a reconciled statement and you pay from it through your own process. **Can I run more than a basic affiliate program on Shopify?** Yes. The same managed Cloud engine runs affiliate, influencer, customer referral, ambassador, royalty, revenue-share, and performance-bonus programs, including tiered rates and distributor bonus pools. Tell us the program shape and we configure the rules around it. **Is the Shopify integration available in the WordPress plugin?** No. The WordPress edition runs on your own site and connects to WooCommerce. Shopify is hosted elsewhere, so Siren reads it through a managed Cloud connection rather than the plugin. **What does a Shopify affiliate program with Siren cost?** Siren Cloud is custom-priced: a one-time setup to connect Shopify and design your commission model, plus an ongoing managed subscription. There is no per-order fee and no revenue-share cut skimmed from your store, so the cost does not climb just because your program works. Tell us how you sell and we will scope real numbers. **I want to get paid for promoting a Shopify store. Is this for me?** No. Siren is for the merchant running the program, not for affiliates joining one. If you run a Shopify store and want to recruit and pay partners who drive sales, that is what this is. ## Stripe integration Source: https://www.sirenaffiliates.com/integrations/stripe Stripe has no built-in affiliate program. Siren Cloud connects to your Stripe account, reads every charge, subscription, and invoice, and pays partners on recurring revenue. Built for SaaS. > Can you run an affiliate program with Stripe? Yes, with Siren Cloud. Stripe itself has no affiliate feature, so Siren connects to your Stripe account over its API and webhooks, attributing each charge, new subscription, and recurring invoice to the partner who referred it. Best fit for SaaS and subscription businesses billing on Stripe. ### What Siren does on Stripe - Track sales from Stripe billing: Read charges, subscriptions, and invoices as the source of conversions, to attribute referrals. (Siren Cloud Full-Service (hosted and managed)) - Track renewals, expansion, and churn: Recurring invoices, plan upgrades, and cancellations as events for residual commission. (Siren Cloud Full-Service (hosted and managed)) - Pay affiliates via Stripe Connect: Pay partners real money through Stripe Connect. This end works in the WordPress plugin too. (all editions) ### Events Siren can track - One-time charge - Subscription created - Recurring invoice paid - Refunds and disputes - Plan upgrade or expansion - Trials with no payment (not supported) ### Rewards Siren can pay - Flat or percentage of charge - Recurring on every invoice - One-time signup bounty - Tiered by MRR contributed - Revenue share with partners ### Program types you can run on Stripe - SaaS affiliate program - Customer referral program - Revenue-share or reseller - Agency and partner program - Ambassador program ### Edition availability - WordPress plugin (self-hosted): Available. Partial. The WordPress plugin can pay affiliates through Stripe Connect, but it cannot read Stripe billing as your source of sales. Tracking Stripe sales is Cloud-only. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will carry the same Stripe capabilities as the WordPress plugin (paying affiliates via Stripe Connect). Reading Stripe billing as your source of sales stays on the managed Enterprise edition for now. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Siren Cloud connects to your Stripe account over its API and runs the program for you, hosted, set up, and supported. ### What you'll need - A Stripe account you can connect, with read access to charges, subscriptions, and invoices. - A Siren Cloud plan, hosted and set up by our team around your comp model. - Optionally your CRM or product database, so Siren can attribute by customer, not just by Stripe ID. - No engineering lift beyond connecting the account. We handle the setup. ### Good to know - Stripe plays two roles: paying affiliates (Stripe Connect, which works in the WordPress plugin too) and being the source of sales (reading billing to attribute referrals, Cloud-only). This page is mostly about the second. - Siren reads Stripe events to calculate payouts. It does not move money inside Stripe. It produces the reconciled statement you pay from. - Attribution starts at the first successful payment. Trials with no charge are not billable events. - Also connects to Chargebee, Paddle, Recurly, or your own billing service through the same model. ### Program guides for Stripe - Run an affiliate program with Stripe: https://www.sirenaffiliates.com/integrations/stripe/affiliate-program - Run a referral program with Stripe: https://www.sirenaffiliates.com/integrations/stripe/referral-program - Run a revenue share on Stripe: https://www.sirenaffiliates.com/integrations/stripe/revenue-share ### Frequently Asked Questions **How much does a Stripe affiliate program cost?** Siren Cloud is custom-priced: a one-time setup to connect Stripe and design your comp model, plus an ongoing managed subscription. There is no per-charge fee skimmed from Stripe. Tell us how you bill and we will scope real numbers. **Does Stripe have an affiliate program feature?** No. Stripe is a payment processor, not an affiliate platform. There is no built-in referral or commission feature. Siren Cloud adds that layer by connecting to your Stripe account over its API and turning billing events into tracked, attributable referrals. **Can I pay affiliates on recurring Stripe subscriptions?** Yes. Siren reads each recurring invoice from Stripe, so partners can earn on every renewal for the life of the customer, with credit reversed automatically on refunds, disputes, or churn. **Is the Stripe integration available in the WordPress plugin?** No. Tracking Stripe billing is a Siren Cloud integration. The WordPress edition connects to WooCommerce on your own site. Separately, the WordPress edition does use Stripe Connect to pay affiliates real money, which is a different thing from reading Stripe billing as the source of sales. **How does Siren attribute a Stripe charge to a partner?** When a referred visitor signs up, Siren records the referral and ties it to the customer. As Stripe charges and invoices arrive for that customer, Siren matches them back to the original partner and applies your commission rules. **I want to get paid for referring people to a SaaS. Is this for me?** No. Siren is for the business running the program, not for affiliates joining one. If you operate a product that bills on Stripe and want to recruit and pay partners who refer customers, that is what this is. ## WooCommerce integration Source: https://www.sirenaffiliates.com/integrations/woocommerce Run affiliate, referral, influencer, royalty, and sales commission programs on WooCommerce with Siren. Create multiple programs, track every contribution, and reward partners accurately. > What incentive programs can you run on WooCommerce? All of them. Siren is a WordPress plugin that runs natively inside WooCommerce. It hooks into Woo order, subscription, and refund events, so affiliate, referral, loyalty, and royalty programs are all tracked and paid the moment a sale completes. Pick a recipe to install one, or start free on Lite. ### What Siren does on WooCommerce - Track orders, renewals and refunds: Read WooCommerce orders, subscription renewals, and refunds as conversion events. (all editions) - Attribute by coupon code: Tie a coupon to a collaborator so every checkout that uses it is credited automatically. (all editions) - Pay as store credit: Issue automatic store credit that partners redeem at checkout. (all editions) - Pay real money via Stripe Connect: Partners self-onboard Stripe Express and get paid automatically on the Plus tier. (all editions) ### Events Siren can track - New order or sale - Subscription renewal - Specific-product sale - Refunds and clawbacks - Lead capture (Gravity Forms) - Sales made off your WordPress site (not supported) ### Rewards Siren can pay - Flat or percentage commission - Recurring on each renewal - Per-product rates - Lead and contest bonuses - Tiered by volume ### Program types you can run on WooCommerce - Affiliate program - Customer referral program - Influencer or creator program - Distributor or reseller network - Multi-tier partner program (Pro) ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on the same site as WooCommerce and own your data. Tracks Woo orders out of the box. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run WooCommerce with the same capabilities as the WordPress plugin, with no site of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Rather not run it yourself? Siren Cloud runs the same engine on your WooCommerce store, hosted, set up, and supported by our team. ### What you'll need - A WordPress site with WooCommerce active. - For renewal and subscription rewards: the Essentials tier plus WooCommerce Subscriptions. - For automatic real-money payouts: Plus (Stripe Connect). Lower tiers pay store credit or record payouts by hand. - No API keys and no developer setup on the self-hosted plugin. It is the same WordPress site. ### Good to know - The self-hosted plugin tracks events on your WordPress site. To also attribute sales that happen off WordPress, run it on Siren Cloud, which connects to outside systems too. - Multi-tier and cascading commissions are a Pro-tier feature. ### Pre-built recipes for WooCommerce - Affiliate Program (Affiliate program), Siren Lite · Free: Percentage commission on every referred WooCommerce sale. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program - Customer Rewards Program (Loyalty program), Siren Lite · Free: Flat store credit on every purchase, no referral links needed. Install from https://www.sirenaffiliates.com/recipes/customer-rewards-program - Coupon-Based Influencer Program (Influencer program), Siren Lite · Free: Track influencer commissions through unique coupon codes. Install from https://www.sirenaffiliates.com/recipes/coupon-based-influencer-program - Dealer Sell-Through Commission Program (Sales commission program), Siren Lite · Free: Reward dealers when their assigned products sell through. Install from https://www.sirenaffiliates.com/recipes/dealer-sell-through-commission - B2B Referral Program (Referral program), Siren Lite · Free: Reward referral partners for qualified B2B business they introduce. Install from https://www.sirenaffiliates.com/recipes/b2b-referral-program - Sales Team Commission Program (Sales commission program), Siren Lite · Free: Sales reps earn flat commission via personal coupon codes. Install from https://www.sirenaffiliates.com/recipes/sales-team-commission-program ### Program guides for WooCommerce - Run an affiliate program on WooCommerce: https://www.sirenaffiliates.com/integrations/woocommerce/affiliate-program - Run an influencer program on WooCommerce: https://www.sirenaffiliates.com/integrations/woocommerce/influencer-program - Run a loyalty program on WooCommerce: https://www.sirenaffiliates.com/integrations/woocommerce/loyalty-program - Run a royalty program on WooCommerce: https://www.sirenaffiliates.com/integrations/woocommerce/royalty-program ### Frequently Asked Questions **Is there a free WooCommerce affiliate plugin?** Yes. Siren Lite is free and tracks WooCommerce orders out of the box: unlimited programs, link and manual tracking, and flat or percentage commissions. Paid tiers add coupon-code tracking, renewal rewards, automatic payouts, teams, and multi-tier commissions. **Is Siren a WooCommerce affiliate plugin?** Yes. Siren installs as a WordPress plugin on the same site as WooCommerce and tracks Woo orders natively. There is no separate platform to sign up for. The free Lite tier works with WooCommerce out of the box. **Can I pay commissions on WooCommerce subscription renewals?** Yes, on the Essentials tier and up. Siren rewards the referring collaborator on the first order and on each recurring renewal, and reverses the credit if a customer refunds or cancels inside your clawback window. **Does it work with WooCommerce coupons?** Yes. Assign a coupon code to a collaborator and every checkout using that code is attributed to them automatically. This is useful for influencers who would rather share a code than a tracking link. **Do I need a monthly subscription?** No. The WordPress edition is a yearly per-site license, with a free Lite tier, not a per-order SaaS fee. You host it, you own the data, and your costs do not scale with order volume. Prefer a managed setup? Siren Cloud is the hosted option. **I want to earn commissions promoting other people's WooCommerce stores. Is this for me?** No. Siren is for store owners building and running their own affiliate program. If you are looking to join programs as an affiliate or promoter, this is not that. It is the software a business installs to recruit and pay its own affiliates. ## WordPress integration Source: https://www.sirenaffiliates.com/integrations/wordpress Siren is the WordPress plugin for real affiliate, referral, royalty, loyalty, and bonus programs. Install it on your own site, track every sale natively, and pay partners automatically. Free to start. > Is there a WordPress plugin for affiliate and partner programs? Yes. Siren is a WordPress plugin that runs every incentive program shape on your own site. Install it alongside WooCommerce, Easy Digital Downloads, LifterLMS, LearnDash, Gravity Forms, or Ninja Forms, and it tracks sales, leads, and renewals natively. Affiliate, referral, royalty, loyalty, and bonus programs all run on the same engine. Start free on Lite. ### What Siren does on WordPress - Track sales, renewals, leads and completions: Read WooCommerce, EDD, LifterLMS, LearnDash, Gravity Forms, and Ninja Forms events as conversions. (all editions) - Attribute by link, coupon, or manual credit: Tie a tracking link or coupon to a collaborator, or credit a referral by hand. (all editions) - Pay as store credit: Issue automatic store credit that partners redeem at checkout. (all editions) - Pay real money via Stripe Connect: Partners self-onboard Stripe Express and get paid automatically on the Plus tier. (all editions) ### Events Siren can track - New order or sale - Subscription renewal - Course sale or completion - Lead or form submission - Refunds and clawbacks - Sales made off your WordPress site (not supported) ### Rewards Siren can pay - Flat or percentage commission - Recurring on each renewal - Per-product or per-course rates - Lead, contest, and milestone bonuses - Revenue share and royalties ### Program types you can run on WordPress - Affiliate program - Customer referral program - Revenue share or royalty program - Performance bonus program - Loyalty and rewards program ### Edition availability - WordPress plugin (self-hosted): Available. Self-host the plugin on your own WordPress site and own your data. Tracks your store, LMS, and forms out of the box. - Siren Cloud self-serve (coming soon): Not available yet. Coming soon. The hosted, sign-up-yourself edition will run your WordPress events with the same capabilities as the plugin, with no server of your own to manage. Not live yet. - Siren Cloud Full-Service (hosted and managed): Available. Rather not run it yourself? Siren Cloud runs the same engine on your WordPress site, hosted, set up, and supported by our team. ### What you'll need - A WordPress site, with WooCommerce, EDD, an LMS, or a form plugin like Gravity Forms or Ninja Forms for automatic tracking. - The free Lite tier runs a real affiliate program out of the box. - Essentials adds coupon tracking, renewals, and more reward shapes. Plus adds teams and real-money payouts. - No API keys and no developer setup. It is a plugin on the site you already run. ### Good to know - The self-hosted plugin tracks events on your WordPress site. To also attribute sales that happen off WordPress, run it on Siren Cloud, which connects to outside systems too. - Multi-tier and cascading commissions are a Pro-tier feature. ### Pre-built recipes for WordPress - Affiliate Program (Affiliate program), Lite · Free: Percentage commission on every referred sale. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program - Refer-a-Friend (Referral program), Essentials: A flat reward for every friend a customer refers who buys. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program - Revenue Share (Revenue share program), Essentials: Share a percentage of revenue with partners and creators. Install from https://www.sirenaffiliates.com/recipes/business-partner-revenue-share - Customer Rewards (Loyalty program), Essentials: Flat store credit on every purchase, no referral links needed. Install from https://www.sirenaffiliates.com/recipes/customer-rewards-program ### Program guides for WordPress - Run an affiliate program on WordPress: https://www.sirenaffiliates.com/integrations/wordpress/affiliate-program - Run a bonus program on WordPress: https://www.sirenaffiliates.com/integrations/wordpress/bonus-program - Run a loyalty program on WordPress: https://www.sirenaffiliates.com/integrations/wordpress/loyalty-program - Run a referral program on WordPress: https://www.sirenaffiliates.com/integrations/wordpress/referral-program - Run a revenue share program on WordPress: https://www.sirenaffiliates.com/integrations/wordpress/revenue-share ### Frequently Asked Questions **Is there a free WordPress affiliate plugin?** Yes. Siren Lite is free and runs a real affiliate program on your WordPress site: unlimited programs and partners, link and manual tracking, and flat or percentage commissions. Paid tiers add coupon tracking, renewal rewards, automatic payouts, teams, and multi-tier commissions. **What incentive programs can Siren run on WordPress?** All of them on one engine: affiliate, customer referral, revenue share and royalty, performance bonus, and loyalty programs. The program shape changes the rules you configure, not the plugin you install. **Does Siren work with WooCommerce, EDD, and my LMS?** Yes. Siren tracks WooCommerce and Easy Digital Downloads orders, LifterLMS and LearnDash course sales and completions, and Gravity Forms and Ninja Forms submissions natively, because it runs on the same WordPress site they do. **Do I need a monthly subscription?** No. The WordPress edition is a yearly per-site license with a free Lite tier, not a per-order SaaS fee. You host it, you own the data, and your costs do not scale with sales volume. Prefer a managed setup? Siren Cloud is the hosted option. **Will it slow down my site or fight my caching plugin?** No. Tracking is first-party and runs against your own database, so it works alongside standard caching instead of depending on fragile page-level workarounds. Siren was built for performance on high-traffic sites. --- # Integration program guides ## Get rep commission right on agency business Source: https://www.sirenaffiliates.com/integrations/marketron/agency-commission Get rep commission right on agency-placed Marketron orders. The recognized agency keeps its 15%; Siren computes the net-to-station base your reps are paid on, gross or net per your rate card. A recognized agency keeps its 15% off the top, so the station nets 85%, and most plans pay reps on that net-to-station number. Marketron already nets the 15% on the invoice; what it does not do is carry that gross-or-net choice into your rep commission. Siren Cloud reads which orders are agency-placed and pays your reps on the right base, with a per-agency breakdown you can reconcile. > How does Siren handle agency commission on Marketron orders? It gets your reps' commission right on agency business. The recognized-agency 15% is a discount the agency takes, not a check the station writes, so the station nets 85% and most plans pay reps on that net-to-station figure. Siren Cloud reads which Marketron orders are agency-placed, applies the gross-or-net convention from your rate card, and pays each rep on the correct base. Where you genuinely pay an outside firm a commission, such as a national rep firm placing spot business, Siren pays that as a partner percentage. ### How it works on Marketron 1. Agency places the order: A recognized agency books a buy, recorded in Marketron against that agency's account. 2. Siren reads the order: Marketron's data tells Siren the order is agency-placed and carries the gross and net amounts. 3. Siren sets the base: It applies your rate-card convention, gross or net-to-station, and pays the rep on the right number. 4. Breakdown: A per-agency view you can reconcile, with adjustments on non-payment. ### What is different on Marketron - A discount, not a payout: The agency keeps its 15% and the station nets 85%. Siren does not pay the agency. It makes sure the rep is paid on the base your plan intends. - Matches your rate card: Net-to-station or grossed-up commissionable, Siren reads which one an agency order is and pays the rep accordingly. - When you do pay a commission: A national rep firm that genuinely earns a station-paid commission is paid as a partner percentage on the business it places. ### Events tracked - Agency-placed order - Invoice issued (gross and net) - Payment collected - Non-payment or made-good ### Rewards supported - Rep paid on net-to-station for agency business - Gross or net to match the rate card - Per-agency negotiated conventions - Partner percentage to a national rep firm ### Scenarios - Pay the rep on the 85%: When a recognized agency places the order, the station nets 85%, and Siren pays the rep on that net figure rather than the grossed-up rate card number. - Two bases, one plan: A direct order pays the rep on the full amount; an agency order pays on net-to-station. Siren reads which is which from Marketron and applies the right one. - A real outside commission: Where you actually owe a national rep firm a percentage of the spot business it places, that is a partner payout Siren runs on the same engine. ### What you'll need - A Marketron system whose orders identify the placing agency and carry gross and net amounts. - A Siren Cloud plan, hosted and set up by our team. - Your rule: do reps earn on gross or net-to-station for agency business, and any national-rep arrangements. - No development work on your side. We build the connection; you supply the rules. ### Install it from a recipe - Partner Revenue Share (Cloud): An ongoing percentage paid to an assigned outside partner, such as a national rep firm, on the business it places. Install from https://www.sirenaffiliates.com/recipes/business-partner-revenue-share ### Other programs on Marketron - AE commission: https://www.sirenaffiliates.com/integrations/marketron/ae-commission - Local referral: https://www.sirenaffiliates.com/integrations/marketron/local-referral - New business vs renewal: https://www.sirenaffiliates.com/integrations/marketron/new-business-vs-renewal ### Frequently Asked Questions **The agency keeps the 15% and we don't write them a check, so what is Siren doing?** Getting your reps paid on the right base. The recognized-agency 15% is a discount the agency takes off the top, so the station nets 85% and most plans pay reps on that net-to-station number. Siren reads which Marketron orders are agency-placed and pays each rep on the base your plan intends, rather than on the grossed-up rate. It does not pay the agency anything. **Are reps paid on gross or net-to-station for agency business?** Whichever your plan says, per agency if needed. Stations that publish net-to-station rates and stations that gross up to a commissionable rate handle this differently, and reps are usually paid on net for agency orders. Siren reads which convention an order uses from Marketron and applies it, so the rep's commissionable base is correct without anyone adjusting it by hand. **Marketron already nets the agency 15% on the invoice. What does Siren add?** Marketron nets the agency on the invoice; it does not carry that gross-or-net choice into rep commission. That is the gap your business manager fills in a spreadsheet today: deciding, per order, whether the rep earns on gross or on net-to-station. Siren does that automatically and shows a per-agency breakdown you can reconcile against Marketron. **Do you ever actually pay an agency or rep firm a commission?** Only where you genuinely owe one. A local recognized agency keeps its discount and is not paid by Siren. A national rep firm that earns a station-paid commission on the spot business it places is different, and Siren can pay that as a partner percentage, kept separate from your reps' commission. ## Pay local referral fees on top of Marketron Source: https://www.sirenaffiliates.com/integrations/marketron/local-referral Pay a local business a fee when it refers an advertiser who buys. Siren Cloud tracks the referral against Marketron orders and pays non-employees the same way it pays reps. Some of your best advertisers come from a tip: a local business that sends a neighbor your way. Siren Cloud lets you pay that referrer a fee when the advertiser they sent actually buys, tracked against Marketron orders, with no concept of it in Marketron's own commission report because the referrer is not an employee. > Can I pay a local business for referring an advertiser to Marketron? Yes, with Siren Cloud. Siren tracks the referral, ties it to the advertiser, and pays the referrer a fee when that advertiser's order is booked and billed in Marketron. Because the referrer is a non-employee, Marketron's commission report has no place for them; Siren pays them on the same engine it uses for rep commissions, with a flat bounty or a percentage. ### How it works on Marketron 1. A local business refers: A partner sends an advertiser your way, and Siren records the referral against that advertiser. 2. The advertiser buys: The referred advertiser's order is booked and billed in Marketron. 3. Siren reads the order: Marketron's APIs feed the billed order to Siren, which matches it to the referrer. 4. Payout: The referral fee is reconciled into a statement, reversed if the advertiser does not pay. ### What is different on Marketron - Non-employees, not reps: The referrer is a local business, agency contact, or partner, paid on the same engine as your reps but tracked separately. - Flat fee or percentage: Pay a fixed bounty per referred advertiser, or a percentage of what that advertiser bills, your call. - Logged once, then automatic: A referral is a handshake, not a click, so it is recorded against the advertiser once at the start. From then on Siren matches that advertiser's Marketron orders to the referrer automatically. ### Events tracked - Referred advertiser order - Invoice issued - Payment collected - Non-payment or made-good ### Rewards supported - Flat bounty per referred advertiser - Percentage of the advertiser's billing - One-time or ongoing for renewals - Clawback if the advertiser does not pay ### Scenarios - A flat bounty per advertiser: Pay a local business a fixed fee each time an advertiser they referred signs and bills, with nothing owed on tips that never buy. - Pay on renewals too: Reward a productive referrer with a percentage that keeps paying as the referred advertiser renews, not just on the first order. - Kept apart from rep pay: Referrer payouts run on their own program, so they never mix into the reps' commission or Marketron's employee report. ### What you'll need - A Marketron system with API access to order and billing data. - A Siren Cloud plan, hosted and set up by our team. - Your referral terms: flat fee or percentage, one-time or ongoing. - No development work on your side. You supply the comp rules and roster, and we build the connection onto Marketron's APIs. ### Install it from a recipe - Local Referral Program (Cloud): A flat bounty paid to a non-employee partner when a referred advertiser buys, first-touch attributed. Install from https://www.sirenaffiliates.com/recipes/b2b-referral-program ### Other programs on Marketron - Agency commission: https://www.sirenaffiliates.com/integrations/marketron/agency-commission - AE commission: https://www.sirenaffiliates.com/integrations/marketron/ae-commission - Sales contest: https://www.sirenaffiliates.com/integrations/marketron/sales-contest ### Frequently Asked Questions **Why can't Marketron pay a referral fee to a local business?** Marketron's commission report is built for salespeople on staff. A local business that refers an advertiser is not an employee, so there is no place for them in that report. Siren pays non-employee referrers on the same engine it uses for reps, tracked as their own program. **Do I pay the referral fee on the order or on payment?** Either. Siren can pay the fee when the referred advertiser's order is billed, or when the advertiser actually pays, reading both events from Marketron and reversing the fee if the advertiser does not pay. **Can the referrer earn on renewals, not just the first order?** Yes. Pay a one-time bounty on the first order, or an ongoing percentage that continues as the referred advertiser renews. The choice is part of how the program is set up. **A referral is a handshake, not a click. How does it actually get logged?** Someone records it once, against the advertiser, when the relationship starts: this advertiser was referred by this partner. That is a one-time entry in Siren, not a per-order step. After that, Siren matches the advertiser's Marketron orders to the referrer automatically, so the attribution does not depend on anyone remembering to log each sale. **We are paying non-employees real money. Does Siren handle W-9s and 1099 reporting?** Siren produces the payee totals you need, what each referrer earned over the year, so your accountant or business manager can issue 1099s. Siren does not move money or file the forms; it gives you the reconciled numbers behind each payout, kept separate from employee commission. **What happens if two partners claim the same referred advertiser?** The referral is recorded against the advertiser once, so there is a single referrer of record rather than competing claims at payout. If a dispute comes up, the entry has a date and a source you can check, and you can correct it before the period closes. **Is the referral fee separate from rep commission?** Yes. The referral runs on its own program, kept apart from your reps' commission and from Marketron's employee commission report, so the two never collide on the same order. ## Pay new business and renewals differently on Marketron Source: https://www.sirenaffiliates.com/integrations/marketron/new-business-vs-renewal Pay reps a higher rate on new advertisers than on renewals. Siren Cloud fires one of two rates by order type, new sale or renewal, classified from your Marketron data and the rule set at setup. Landing a new advertiser is harder than renewing one, and most radio plans pay accordingly. Siren Cloud runs two linked rates, a higher one for new business and a lower one for renewals, firing exactly one per order by whether the order is a new sale or a renewal, so the split is never reconciled by hand. > Can I pay a higher commission on new business than on renewals with Marketron? Yes, with Siren Cloud. Siren fires the new-business rate on a new sale and the renewal rate on a renewal, deciding which from your Marketron order data and the classification rule set at setup. A program group guarantees exactly one rate pays per order, so the split is enforced rather than maintained on a spreadsheet, and you can correct a classification before the period closes. ### How it works on Marketron 1. Marketron books the order: A new advertiser signs, or an existing account renews its schedule. 2. Siren reads the order type: It takes whether the order is a new sale or a renewal from your Marketron data and the classification set at setup. 3. Siren fires one rate: The new-business rate or the renewal rate pays, never both, on that order. 4. Payout: Reconciled into a statement each period, with clawbacks on non-payment. ### What is different on Marketron - New business pays more: A higher rate on a first order from an advertiser, a lower maintenance rate on renewals. Both credit the same rep. - Set once at setup: You define what counts as new, renewal, and win-back, and we set it up against your Marketron data, so reps do not self-classify and the rule is the same for everyone. - A program group enforces it: The two rates sit in one group that fires exactly one per order, so a renewal can never slip through at the new-business rate. ### Events tracked - New advertiser order - Renewal of an existing account - Win-back of a lapsed account - Non-payment or made-good ### Rewards supported - Higher percentage on new business - Lower percentage on renewals - Optional win-back rate for lapsed accounts - Clawback on non-payment and make-goods ### Scenarios - Reward landing a new account: A rep who signs a brand-new advertiser earns the new-business rate; the same rep earns the renewal rate when that account renews next cycle. - A third rate for returning accounts: Add a win-back rate for a lapsed advertiser that comes back, sitting alongside new business and renewal in the same group. - No month-end reclassification: Because each order's type is decided the same way every time, the new-versus-renewal split is set by policy, not argued over when payouts run. ### What you'll need - A Marketron system whose order data distinguishes new business from renewals. - A Siren Cloud plan, hosted and set up by our team around your comp plan. - Your new-business rate, renewal rate, and any win-back rate. - No development work on your side. You supply the comp rules and roster, and we build the connection onto Marketron's APIs. ### Install it from a recipe - New Business vs Renewal Commission (Cloud): Two rates in one program group, fired by order type so new business and renewals never collide. Install from https://www.sirenaffiliates.com/recipes/new-business-vs-renewal-commission ### Other programs on Marketron - AE commission: https://www.sirenaffiliates.com/integrations/marketron/ae-commission - Sales contest: https://www.sirenaffiliates.com/integrations/marketron/sales-contest - Agency commission: https://www.sirenaffiliates.com/integrations/marketron/agency-commission ### Frequently Asked Questions **Which Marketron field tells you new versus renewal, and what if it is blank or wrong?** Marketron does not always carry a clean new-versus-renewal flag, so the classification is established at setup from your order data and your definition, for example a returning advertiser after a gap. Siren then fires the matching rate by order type. You can correct a classification in Siren before the period closes if a real-world judgment differs. **An advertiser renews but increases the buy. Is the increase new business?** That is your call, and Siren can honor it. A common plan pays the renewal rate on the prior level and the new-business rate on the increase above it. Tell us your rule for upsell on an existing account and we set the boundary. It does not have to be all-or-nothing per order. **A rep inherits an account. Does the renewal pay them the renewal rate or new business?** Whatever your plan says when an account changes hands. Siren classifies by the advertiser's history, not the rep's, so a renewal stays a renewal even with a new rep on it, unless you set a different rule for transferred accounts. The point is the rule is explicit and applied the same way every time. **When this turns on, do all my existing accounts immediately become renewals?** Only if your rule says so, and you decide that at setup. Many stations grandfather a rep's existing book or set a transition date so a built book is not converted to the lower rate overnight. This is a plan decision you make, not a default Siren imposes, and it is worth settling with your reps before go-live. **Can I add a win-back rate for lapsed advertisers?** Yes. The two rates sit in a program group, and you can add a third program with its own rate and order type, for example a win-back rate when a lapsed account returns. The group still fires exactly one rate per order. **What stops a renewal from paying the new-business rate?** The program group. It holds both rates and guarantees one fires per order based on the order type, so a renewal cannot be paid at the higher new-business rate even if the rep is enrolled in both. **Does this replace Marketron?** No. Marketron stays your traffic, billing, and AR system of record. Siren reads its order and billing events and calculates the right rate. What it replaces is the spreadsheet where new and renewal business get separated by hand. ## Run a bonus program on WordPress Source: https://www.sirenaffiliates.com/integrations/wordpress/bonus-program Run performance bonus programs on WordPress with Siren. Reward milestones, monthly contests, and top performers with a pooled bonus, calculated automatically and paid from your own site. Free to start. Siren is the WordPress plugin for performance bonuses. Reward milestones, monthly contests, and your top performers with a pooled bonus that Siren calculates automatically, no spreadsheet math. Here is how it works, and the recipe to install it. > Is there a WordPress plugin to run a performance bonus program? Yes. Siren is a WordPress bonus plugin. Install the Monthly Sales Bonus recipe and Siren tracks performance over a period, then awards a pooled bonus to the winner automatically. It runs on your own site, on top of the same engine as your affiliate program. ### How it works on WordPress 1. Performance is tracked: Siren records each rep or affiliate's qualifying sales over the bonus period. 2. The pool builds: A bonus pool accrues across the month or contest window you define. 3. A winner is chosen: Siren ranks performers and awards the pool by your rule: top seller, highest engagement, or a milestone hit. 4. The bonus is paid: As store credit, or real money via Stripe Connect on Plus. ### What is different on WordPress - Pooled, not per-sale: A bonus program rewards performance over a period, on top of any per-sale commission, using Siren's Distributors. - Reps, affiliates, or teams: Run it for an internal sales team, a roster of affiliates, or a contest among partners. The same engine, different roster. - Store credit or real money: Pay the winner as store credit, or real money via Stripe Connect on Plus. ### Events tracked - Completed order or sale - Qualifying performance over a period - Milestone or threshold reached - Contest window close ### Rewards supported - Winner-takes-all monthly pool - Milestone and threshold bonuses - Contest and leaderboard prizes - Bonuses layered on top of base commission ### Scenarios - Monthly top-seller bonus: Run a winner-takes-all pool for your reps, tracked by personal coupon codes, awarded automatically at month end. - Contest among partners: Add a leaderboard bonus on top of standard commissions to spike promotion during a launch window. - Reward hitting a number: Trigger a bonus when a collaborator crosses a revenue or engagement threshold you set. ### What you'll need - A WordPress site, with WooCommerce, EDD, or an LMS for automatic sale tracking. - The Essentials tier, which adds Distributors for scheduled and pooled rewards. - Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is a plugin on your own site. ### Install it from a recipe - Monthly Sales Bonus (Siren Essentials): A competitive monthly pool where the top performer wins the entire bonus. Install from https://www.sirenaffiliates.com/recipes/monthly-sales-bonus ### Other programs on WordPress - Affiliate program: https://www.sirenaffiliates.com/integrations/wordpress/affiliate-program - Revenue share: https://www.sirenaffiliates.com/integrations/wordpress/revenue-share ### Frequently Asked Questions **Is there a WordPress plugin for performance bonuses?** Yes. Siren runs bonus programs through its Distributors feature on the Essentials tier. Track performance over a period and award a pooled bonus automatically, on the same site and engine as your affiliate and referral programs. **Can I run a bonus on top of regular commissions?** Yes. A bonus program layers on top of per-sale commission. A rep can earn their normal commission on each sale and still compete for the monthly bonus pool. **Can I run bonuses for an internal sales team, not just affiliates?** Yes. Assign each rep a personal coupon code, and Siren attributes their sales and ranks them for the bonus. It is built for employees as well as external partners. **How is the winner chosen?** By the rule you set: most revenue, highest engagement score, or a milestone reached. Siren ranks performers over the period and awards the pool automatically when it closes. **How does the winner get paid?** As store credit on Essentials, or real money through Stripe Connect on the Plus tier. The payout is calculated and recorded for you. ## Run a lead-gen program on Gravity Forms Source: https://www.sirenaffiliates.com/integrations/gravity-forms/lead-gen-program Run a lead-gen program on Gravity Forms with Siren. Pay partners a flat bounty per qualified form submission, attributed last-touch and counted natively on the same WordPress site. Installs from a recipe. Pay partners a flat bounty for every qualified lead they send through a Gravity Forms submission, attributed to the affiliate whose touch was most recent before the form went in. Here is how it runs on Gravity Forms, and the recipe to install it. > Can you run a lead-gen program on Gravity Forms? Yes. Siren connects to the Gravity Forms you already have and treats a connected form submission as a lead. When a partner sends a visitor who submits, Siren credits the partner whose touch was most recent and counts a flat bounty you can pay against. Install the Cost-Per-Lead Campaign recipe to set it up, and decide for yourself what counts as qualified. ### How it works on Gravity Forms 1. A partner refers: They share a link that points a prospect at a form you have connected to Siren. 2. The prospect submits: They fill out the connected Gravity Forms submission natively on your WordPress site, with no redirect. 3. Siren attributes it: Last-touch. The affiliate whose engagement was most recent before the submission gets credit, not whoever found the prospect first. 4. A bounty is counted: Each qualified lead adds a flat amount to the partner's running total, ready to pay out once you mark it qualified. ### What is different on Gravity Forms - Fires on form submissions: Siren reads the connected Gravity Forms submission feed on the same site and counts a lead the moment a referred prospect submits. - Last-touch by default: The recipe uses newest-binding-wins logic, so the affiliate driving the prospect at the conversion moment earns the bounty. Ideal for short, time-boxed campaigns. - Flat per qualified lead: Pay a fixed dollar amount per lead, not a percentage. Your cost per lead is predictable, and junk leads can be reviewed out before you pay. ### Events tracked - Form submission (lead or application) - Qualified paid lead - Partner signup form ### Rewards supported - Flat bounty per qualified lead - Signup and application bonuses - Tiered by lead volume - Manual review before payout ### Scenarios - Pay per lead in a time-boxed push: Run a seasonal or promotional lead drive where the last affiliate to engage a prospect earns the bounty. Last-touch credit rewards the partner closest to the submission. - Decide what counts before you pay: Connect a form as a paid lead source, then review submissions and mark which ones are qualified. Siren counts against your rule, so you never pay on guesses. - Lead-gen alongside your sales programs: A Gravity Forms lead-gen program runs independently of any WooCommerce or form-payment program. Each fires on its own event, and partners get one consistent record across all of them. ### What you'll need - A WordPress site with Gravity Forms active and the forms you want to track. - A rule for what counts as a qualified lead, so Siren counts against it. - Essentials runs the Cost-Per-Lead Campaign with automatic lead counting. Real-money payouts arrive on Plus via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site as your forms. ### Install it from a recipe - Cost-Per-Lead Campaign (Essentials): A flat bounty for every qualified form submission, credited last-touch to the affiliate closest to the conversion. Install from https://www.sirenaffiliates.com/recipes/cost-per-lead-campaign ### Other programs on Gravity Forms - Affiliate program: https://www.sirenaffiliates.com/integrations/gravity-forms/affiliate-program ### Frequently Asked Questions **How do partners get credited for a lead on Gravity Forms?** A partner shares a tracking link that lands a prospect on a form you have connected to Siren. When that prospect submits, Siren reads the submission on the same site and credits the affiliate whose engagement was most recent before the form went in. You can also credit a referral manually if you need to. **Does the lead-gen program use first-touch or last-touch attribution?** Last-touch. The Cost-Per-Lead Campaign recipe uses newest-binding-wins logic, so when several affiliates have engaged the same prospect, the one driving them closest to the submission earns the bounty. That suits short campaigns where the final push matters more than who first introduced the prospect. **How much do I pay per lead, and can I change it?** You set a flat dollar amount per qualified lead when you apply the recipe, and you can adjust it. The payout is fixed and does not depend on any transaction value, so your cost per lead stays predictable no matter how the prospect behaves later. **What stops me from paying out on junk submissions?** You decide what qualifies. Connected form submissions are counted as leads, but you mark which ones are qualified before they earn a bounty, so spam and bad fits can be reviewed out. Siren gives you the structure and the count. It does not force you to pay for a lead you would not have wanted. **Can I run a lead-gen program and a sales program on the same site?** Yes. The lead-gen program listens for form submissions and pays a flat bounty, while a separate sales or affiliate program can fire on form payments or WooCommerce orders. They run independently, and a partner who drives both a lead and a later sale sees one consistent record across them. **Do I need a developer to set this up on Gravity Forms?** If you are already comfortable building forms and setting up feeds in Gravity Forms, you can usually wire Siren in yourself and apply the recipe. On a heavily customized or mission-critical site, a developer can help you tighten the form rules and qualification logic. ## Run a lead-gen program on Ninja Forms Source: https://www.sirenaffiliates.com/integrations/ninja-forms/lead-gen-program Run a lead-gen program on Ninja Forms with Siren. Pay partners a flat bounty per qualified form submission, attributed last-touch and counted natively on the same WordPress site. Installs from a recipe. Pay partners a flat bounty for every qualified lead they send through a Ninja Forms submission, attributed to the affiliate whose touch was most recent before the form went in. Here is how it runs on Ninja Forms, and the recipe to install it. > Can you run a lead-gen program on Ninja Forms? Yes. Siren connects to the Ninja Forms you already have and treats a connected form submission as a lead. When a partner sends a visitor who submits, Siren credits the partner whose touch was most recent and counts a flat bounty you can pay against. Install the Cost-Per-Lead Campaign recipe to set it up, and decide for yourself what counts as qualified. ### How it works on Ninja Forms 1. A partner refers: They share a link that points a prospect at a form you have connected to Siren. 2. The prospect submits: They fill out the connected Ninja Forms submission natively on your WordPress site, with no redirect. 3. Siren attributes it: Last-touch. The affiliate whose engagement was most recent before the submission gets credit, not whoever found the prospect first. 4. A bounty is counted: Each qualified lead adds a flat amount to the partner's running total, ready to pay out once you mark it qualified. ### What is different on Ninja Forms - Fires on form submissions: Siren reads the connected Ninja Forms submission on the same site and counts a lead the moment a referred prospect submits. - Last-touch by default: The recipe uses newest-binding-wins logic, so the affiliate driving the prospect at the conversion moment earns the bounty. Ideal for short, time-boxed campaigns. - Flat per qualified lead: Pay a fixed dollar amount per lead, not a percentage. Your cost per lead is predictable, and junk leads can be reviewed out before you pay. ### Events tracked - Form submission (lead or application) - Qualified paid lead - Partner signup form ### Rewards supported - Flat bounty per qualified lead - Signup and application bonuses - Tiered by lead volume - Manual review before payout ### Scenarios - Pay per lead in a time-boxed push: Run a seasonal or promotional lead drive where the last affiliate to engage a prospect earns the bounty. Last-touch credit rewards the partner closest to the submission. - Decide what counts before you pay: Connect a form as a paid lead source, then review submissions and mark which ones are qualified. Siren counts against your rule, so you never pay on guesses. - Lead-gen alongside your sales programs: A Ninja Forms lead-gen program runs independently of any WooCommerce or form-payment program. Each fires on its own event, and partners get one consistent record across all of them. ### What you'll need - A WordPress site with Ninja Forms active and the forms you want to track. - A rule for what counts as a qualified lead, so Siren counts against it. - Essentials runs the Cost-Per-Lead Campaign with automatic lead counting. Real-money payouts arrive on Plus via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site as your forms. ### Install it from a recipe - Cost-Per-Lead Campaign (Essentials): A flat bounty for every qualified form submission, credited last-touch to the affiliate closest to the conversion. Install from https://www.sirenaffiliates.com/recipes/cost-per-lead-campaign ### Other programs on Ninja Forms - Affiliate program: https://www.sirenaffiliates.com/integrations/ninja-forms/affiliate-program ### Frequently Asked Questions **How do partners get credited for a lead on Ninja Forms?** A partner shares a tracking link that lands a prospect on a form you have connected to Siren. When that prospect submits, Siren reads the submission on the same site and credits the affiliate whose engagement was most recent before the form went in. You can also credit a referral manually if you need to. **Does the lead-gen program use first-touch or last-touch attribution?** Last-touch. The Cost-Per-Lead Campaign recipe uses newest-binding-wins logic, so when several affiliates have engaged the same prospect, the one driving them closest to the submission earns the bounty. That suits short campaigns where the final push matters more than who first introduced the prospect. **How much do I pay per lead, and can I change it?** You set a flat dollar amount per qualified lead when you apply the recipe, and you can adjust it. The payout is fixed and does not depend on any transaction value, so your cost per lead stays predictable no matter how the prospect behaves later. **What stops me from paying out on junk submissions?** You decide what qualifies. Connected form submissions are counted as leads, but you mark which ones are qualified before they earn a bounty, so spam and bad fits can be reviewed out. Siren gives you the structure and the count. It does not force you to pay for a lead you would not have wanted. **Can I run a lead-gen program and a sales program on the same site?** Yes. The lead-gen program listens for form submissions and pays a flat bounty, while a separate sales or affiliate program can fire on form payments or WooCommerce orders. They run independently, and a partner who drives both a lead and a later sale sees one consistent record across them. **Do I need a developer to set this up on Ninja Forms?** If you are already comfortable building forms and adding actions in Ninja Forms, you can usually wire Siren in yourself and apply the recipe. On a heavily customized or mission-critical site, a developer can help you tighten the form rules and qualification logic. ## Run a loyalty program on WooCommerce Source: https://www.sirenaffiliates.com/integrations/woocommerce/loyalty-program Run a loyalty program on WooCommerce with Siren. Reward customers with store credit on every purchase, tracked by customer with no referral links, and install it from a recipe. Reward repeat customers with store credit on every WooCommerce purchase, tracked by customer with no referral links to share. Here is how it works on WooCommerce, and the recipe to install it. > Can you run a loyalty program on WooCommerce? Yes. Install the Customer Rewards Program recipe on a WordPress site running WooCommerce. Enroll customers as collaborators, manually or through a registration form, and each one earns a flat store credit on every order they place, with no referral links or codes to manage. Siren reads Woo orders natively on the same site, and you attribute each order to the customer who placed it, by hand or with an automation. ### How it works on WooCommerce 1. A customer is enrolled: Add the customer as a collaborator once, manually or through a registration form. No link or code for them to remember. 2. The customer buys: An enrolled customer checks out on your WooCommerce store, same as any other order. 3. A flat credit is earned: Attribute the order to that customer, by hand or with an automation, and Siren books a fixed credit regardless of order size. 4. Credit is redeemed: The balance becomes WooCommerce store credit the customer spends on their next checkout. ### What is different on WooCommerce - Self-rewarding, not referral: The customer who places the order is the one credited. There is no referrer, so no link or coupon ever needs to be shared. - Reads native Woo orders: Completed orders are read natively on the same site, with subscription renewals through WooCommerce Subscriptions, so an enrolled customer can be credited on their first purchase and every reorder. - Store credit, redeemed at checkout: Rewards land as WooCommerce store credit the customer applies on their next order, which keeps them buying from you. ### Events tracked - Completed WooCommerce order - Subscription renewal (with Woo Subscriptions) - Specific-product or category purchase - Refund or clawback reversal ### Rewards supported - Flat credit on every purchase - Recurring on each subscription renewal (Essentials, with Woo Subscriptions) - Per-product or per-category rates for featured items - Tiered by lifetime spend or order volume ### Scenarios - Reward every repeat purchase: Give a fixed credit on each order so customers earn simply by buying again, with nothing to opt into or remember. - Loyalty on every renewal: Pair with WooCommerce Subscriptions and the customer earns credit on the first order and each renewal, rewarding them for staying. - Returns reverse the credit: If a rewarded order is refunded inside your window, Siren reverses the loyalty credit automatically so balances stay honest. ### What you'll need - A WordPress site with WooCommerce active. - The Essentials tier runs the Customer Rewards Program recipe. - Essentials with WooCommerce Subscriptions for rewards on each renewal. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Customer Rewards Program (Essentials): A flat store credit on every purchase, tracked automatically by customer with no referral links. Install from https://www.sirenaffiliates.com/recipes/customer-rewards-program ### Other programs on WooCommerce - Affiliate program: https://www.sirenaffiliates.com/integrations/woocommerce/affiliate-program - Influencer program: https://www.sirenaffiliates.com/integrations/woocommerce/influencer-program - Royalty program: https://www.sirenaffiliates.com/integrations/woocommerce/royalty-program ### Frequently Asked Questions **How is a WooCommerce loyalty program different from an affiliate program?** A loyalty program rewards the customer for their own purchases, so there are no referral links or coupons to share. Siren ties each order to the customer who placed it and credits them directly. An affiliate program instead pays a third party for sales they refer. **Do customers need a referral link to earn loyalty rewards?** No. That is the point of the Customer Rewards Program. You enroll each customer as a collaborator once, manually or through a registration form, and from then on they earn on every order without sharing a link or entering a code. You attribute each WooCommerce order to the customer who placed it, by hand or with an automation, and Siren credits them. **How do customers redeem their loyalty rewards?** Rewards accrue as WooCommerce store credit. The customer applies their balance at checkout on a future order, so the loyalty they earn comes back to your store as their next purchase rather than a cash payout. **Can customers earn loyalty credit on subscription renewals?** Yes, on the Essentials tier with WooCommerce Subscriptions. Siren books the loyalty credit on the first order and on every recurring renewal, which rewards customers for staying subscribed. **What happens to loyalty credit if a customer refunds an order?** Siren reverses it. If a rewarded WooCommerce order is refunded inside your clawback window, the loyalty credit for that order is removed automatically, so customers cannot keep rewards on purchases they returned. ## Run a loyalty program on WordPress Source: https://www.sirenaffiliates.com/integrations/wordpress/loyalty-program Run a loyalty and rewards program on WordPress with Siren. Give customers automatic store credit on every purchase, no referral links or points juggling, tracked natively on your own site. Free to start. Siren is the WordPress plugin for loyalty and rewards. Give customers automatic credit on every purchase, tracked by customer with no referral links to share, on the site you already own. Here is how it works, and the recipe to install it. > Is there a WordPress plugin to run a loyalty program? Yes. Siren is a WordPress loyalty plugin. Install the Customer Rewards Program recipe and customers earn a flat credit on every purchase automatically, with no referral links and no points system to manage. It runs on your own site, on the same engine as your affiliate and referral programs. ### How it works on WordPress 1. A customer buys: The order completes natively on your WordPress site, attributed to that customer. 2. Siren credits them: A flat reward is recorded for the customer automatically, with no link or code required. 3. Rewards accrue: Each purchase adds to the customer's balance, tracked over their whole history with you. 4. They redeem it: Customers spend accrued credit at checkout, or you pay it out via Stripe Connect on Plus. ### What is different on WordPress - Tracked by customer: Loyalty rewards attach to the buyer directly, so there is nothing for customers to share or remember. - Flat credit per purchase: Reward every purchase with a fixed credit, the simplest loyalty model, with no points-to-dollars math. - Store credit or real money: Customers redeem credit at checkout, or you pay it as real money via Stripe Connect on Plus. ### Events tracked - Completed order or sale - Subscription renewal - Specific-product or category sale - Refund or clawback reversal ### Rewards supported - Flat credit on every purchase - Per-product or per-category rates - Recurring on subscription renewals - Bonus rewards at milestones ### Scenarios - Reward every purchase: Give customers a flat credit each time they buy, building a balance they redeem on their next order. - Reward each renewal: Pair with WooCommerce Subscriptions to credit customers on every renewal, not just the first order. - Bigger rewards for top customers: Layer milestone bonuses so your highest-spending customers earn an extra reward when they cross a threshold. ### What you'll need - A WordPress site, with WooCommerce or EDD for automatic purchase tracking. - The Essentials tier for customer-tracked rewards and store credit. - Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is a plugin on your own site. ### Install it from a recipe - Customer Rewards Program (Siren Essentials): A flat credit on every purchase, tracked automatically by customer. Install from https://www.sirenaffiliates.com/recipes/customer-rewards-program ### Other programs on WordPress - Referral program: https://www.sirenaffiliates.com/integrations/wordpress/referral-program - Affiliate program: https://www.sirenaffiliates.com/integrations/wordpress/affiliate-program ### Frequently Asked Questions **Is there a WordPress loyalty plugin?** Yes. Siren runs loyalty and rewards programs on WordPress through the Customer Rewards recipe. Customers earn a flat credit on every purchase automatically, tracked by customer with no referral links, on the same engine as your affiliate and referral programs. **Do customers need a referral link to earn loyalty rewards?** No. Loyalty rewards attach to the customer directly, so every purchase they make is credited automatically. There is nothing for them to share, copy, or remember. **How do customers redeem their rewards?** As store credit at checkout on Essentials, or as real money through Stripe Connect on the Plus tier. The balance accrues automatically and is ready whenever they are. **Can I reward loyalty on subscription renewals?** Yes. With WooCommerce Subscriptions, you can credit customers on each renewal as well as the first order, so loyalty rewards keep accruing for recurring purchases. **Can I combine loyalty with an affiliate or referral program?** Yes. All of Siren's program shapes run on one engine, so you can reward customers for their own purchases (loyalty), for referring friends (referral), and recruit affiliates, all on the same WordPress site. ## Run a referral program on Easy Digital Downloads Source: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/referral-program Run a customer referral program on Easy Digital Downloads with Siren. Existing buyers share a link, earn a flat reward when a friend buys a download, and Siren attributes and tracks it natively on the same WordPress site. Installs from a recipe on the Essentials tier. Turn your existing Easy Digital Downloads customers into a growth channel. Each buyer gets a referral link, and when a friend purchases a download, the referrer earns a flat reward. It runs natively on the same WordPress site, with no external service. Here is how it works on Easy Digital Downloads, and the recipe to install it. > Can you run a referral program on Easy Digital Downloads? Yes. Siren is a WordPress plugin that runs natively inside Easy Digital Downloads, so you can give every customer a referral link and reward them when a friend buys a download. Siren records the referred site visit, binds the friend to the referrer, and tracks a flat reward as a commission the moment the EDD order completes. Install it from the Refer-a-Friend recipe and start on the Essentials tier. ### How it works on Easy Digital Downloads 1. A customer refers a friend: They sign up through your registration form and share their unique referral link. 2. The friend visits and buys: Siren records the referred site visit, binds the friend to the referrer, and the download purchase completes natively in Easy Digital Downloads. 3. Siren attributes it: Newest binding wins. The most recent referrer is credited the moment the EDD order completes. 4. The flat reward is tracked: Siren records a fixed reward as a commission, ready to pay as store credit or real money via Stripe Connect on Plus. ### What is different on Easy Digital Downloads - Fires on the EDD order: The referral reward triggers when a referred friend completes a download purchase, read natively on the same WordPress site with no middleware. - A flat amount, not a percentage: Customers think in dollars, not commission rates. The referrer earns the same fixed reward whether the friend spends a little or a lot. - Newest referral wins: If a friend clicks links from two customers before buying, the most recent referral is credited, which keeps the program simple for everyday customers. ### Events tracked - Completed Easy Digital Downloads order from a referred friend - Referred site visit that binds a friend to a referrer - License or subscription renewal (Essentials, with EDD recurring payments) - Refund or clawback reversal inside your policy window ### Rewards supported - Flat reward per referred purchase - Reward triggers on real line items only, not shipping or fees - Optional friend-side discount via an EDD discount code - Recurring rewards on renewals (Essentials, with EDD recurring payments) ### Scenarios - Reward your best customers for sharing: Frame referrals as a loyalty perk. Existing EDD buyers earn a flat reward every time a friend they referred buys a download. - Reward referrals on renewals too: On Essentials with EDD recurring payments, decide whether the referrer earns once or across a defined set of license and membership renewals. - Review before you pay: Siren tracks rewards as pending commissions. Approve them before payout and reject any that fall foul of a refund, keeping self-referral and fraud in check. ### What you'll need - A WordPress site with Easy Digital Downloads active. - The Essentials tier runs the Refer-a-Friend recipe, including the registration form for customers to join. - Essentials also covers renewal rewards with EDD recurring payments. Plus adds automatic real-money payouts via Stripe Connect, and store credit works on lower tiers. - No API keys and no developer setup. It is the same WordPress site. ### Install it from a recipe - Refer-a-Friend Program (Essentials): Existing customers share a link and earn a flat reward when a friend buys, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program ### Other programs on Easy Digital Downloads - Affiliate program: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/affiliate-program - Royalty program: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/royalty-program ### Frequently Asked Questions **How is a referral program different from an affiliate program on Easy Digital Downloads?** An affiliate program rewards professional marketers with a percentage commission on referred download sales. A referral program is a loyalty feature for your existing Easy Digital Downloads customers, who share a link with friends and earn a simple flat reward. Siren can run both in parallel on the same store, each with its own rules and rewards. **How do customers get their referral link on Easy Digital Downloads?** Set up a program registration form with Siren. Customers sign up, receive a unique referral link, and share it with friends. When a friend clicks through and buys a download, Siren records the referred site visit and binds that friend to the referrer. **Is the referral reward a flat amount or a percentage?** The Refer-a-Friend recipe uses a flat reward, so the referrer earns the same fixed amount no matter which download the friend buys or how much they spend. The reward triggers on actual product line items only, so shipping, taxes, and fees are excluded from what sets it off. **Can I also give the referred friend a discount?** Yes. The recipe rewards the referrer, but you can create an Easy Digital Downloads discount code and share it alongside the referral link in your confirmation messaging. The friend gets a discount at checkout while the referrer still earns their flat reward. **How do referral rewards get paid out on Easy Digital Downloads?** Siren tracks each reward as a commission you can review and approve. Pay it as Easy Digital Downloads store credit on the Essentials tier, or as real money through Stripe Connect on the Plus tier, where partners self-onboard and are paid automatically. **What stops people from referring themselves with fake accounts?** Siren records referrals by engagement events, so basic self-referral across separate accounts is possible. For most stores the flat reward keeps abuse low, and because rewards land as pending commissions you can review them before approving any payout. If a referred order is later refunded inside your window, you can reject that commission so it never pays out. ## Run a referral program on North Commerce Source: https://www.sirenaffiliates.com/integrations/north-commerce/referral-program Run a referral program on North Commerce with Siren. Reward existing customers with a flat credit for every friend they refer who buys, tracked natively on the same WordPress site and installed from a recipe. Turn your existing North Commerce customers into a referral channel. Each customer gets a unique link, and when a friend they refer makes a purchase, the referrer earns a flat reward. It is tracked natively on the same WordPress site as your store, and here is the recipe to install it. > Can you run a referral program on North Commerce? Yes. Siren is a WordPress plugin that runs natively alongside North Commerce, so a customer referral program lives on the same site as your store. Each customer shares a unique link, and when a referred friend completes a North Commerce order, Siren records a flat reward for the referrer automatically. Install the Refer-a-Friend recipe and start on the Essentials tier. ### How it works on North Commerce 1. A customer joins: They sign up through your Siren registration form and receive a unique referral link to share with friends. 2. A friend clicks: Siren records a referred site visit and binds that friend to the customer who referred them, newest referral wins. 3. The friend buys: The order completes natively on your North Commerce store, with no redirect and no external service in the loop. 4. The reward lands: Siren credits the referrer a flat amount the moment the order completes. Pay it as store credit, or real money via Stripe Connect on Plus. ### What is different on North Commerce - Fires on a North Commerce sale: The reward is earned when the referred friend's order completes on your store, read natively on the same WordPress site. - Flat amount, not a percentage: Customers think in dollars, not commission rates. Whether the friend spends a little or a lot, the referrer earns the same fixed credit. - Newest referral wins: If a friend clicked links from two customers before buying, the most recent referrer is credited. Simple for customers, no attribution rules to explain. ### Events tracked - Completed North Commerce order from a referred friend - Referred site visit that binds a friend to a referrer - Refund or clawback reversal inside your policy window ### Rewards supported - Flat reward per referred sale - Set the amount when you install the recipe - Triggers on product line items only, not shipping or tax - Optional friend discount via a North Commerce coupon ### Scenarios - Reward customers for word-of-mouth: Frame the program as a loyalty perk. Existing buyers earn a flat credit every time a friend they sent makes a North Commerce purchase. - Grow a membership organically: For subscriptions or memberships sold through North Commerce, let happy members bring in new buyers and earn a fixed reward for each one. - Sweeten both sides: Pair the referrer's reward with a North Commerce coupon for the friend, so the person being referred has a reason to buy too. ### What you'll need - A WordPress site with North Commerce active. - The Essentials tier runs the Refer-a-Friend recipe, including the registration form for customers to join. - Plus for automatic real-money payouts via Stripe Connect. Store credit works on lower tiers. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Refer-a-Friend Program (Essentials): Reward existing customers with a flat credit for every friend they refer who makes a purchase. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program ### Other programs on North Commerce - Affiliate program: https://www.sirenaffiliates.com/integrations/north-commerce/affiliate-program ### Frequently Asked Questions **How do customers get their referral link on North Commerce?** Set up a Siren program registration form. Customers sign up through it and receive a unique referral link they can share. Because Siren runs on the same WordPress site as North Commerce, there is no separate portal or account system to manage. **Is the referral reward a flat amount or a percentage?** A flat amount. The Refer-a-Friend recipe pays a fixed credit per referred sale, which you set when you install it. Whether the friend spends fifteen dollars or a hundred and fifty on North Commerce, the referrer earns the same reward. Flat amounts read more naturally to everyday customers than commission percentages. **Can I give the referred friend a discount too?** Yes. The recipe rewards the referrer, but you can create a North Commerce coupon and share it alongside the referral link in your sign-up confirmation. That gives the friend a reason to buy and the referrer a reason to share. **What happens if two customers refer the same friend?** Newest referral wins. If a friend clicked links from two different customers before buying on North Commerce, Siren credits the most recent referrer. This keeps the program simple for customers who should not have to think about attribution windows. **How is the referral reward paid out on North Commerce?** Siren tracks the earned amount as a commission, and how you pay it is up to you. Many stores issue North Commerce store credit, and on the Plus tier referrers can self-onboard Stripe Connect to be paid real money automatically. You approve rewards before they pay, so a refunded order never costs you. ## Run a referral program on WordPress Source: https://www.sirenaffiliates.com/integrations/wordpress/referral-program Run a referral program on WordPress with Siren. Reward customers and partners with store credit or cash for every friend or business they refer who buys, tracked automatically on your own site. Free to start. Siren is the WordPress plugin for referral programs. Reward customers and partners for every friend or business they send your way, tracked automatically on the site you already own. Here is how it works, and the recipe to install it. > Is there a WordPress plugin to run a referral program? Yes. Siren is a WordPress referral plugin. Install the Refer-a-Friend Program recipe and existing customers earn a reward for every friend they refer who buys. Siren tracks the referral natively and credits it automatically. It runs on your own site, on the same engine as your affiliate program. ### How it works on WordPress 1. A customer refers: They share a personal referral link or code with a friend or business contact. 2. The friend buys: The sale completes natively on your WordPress site, with no redirect and no middleman. 3. Siren attributes it: The referring customer is credited the moment the referred order completes. 4. The reward is paid: As store credit on Lite and Essentials, or real money via Stripe Connect on Plus. ### What is different on WordPress - Customers, not just affiliates: A referral program turns your existing customers into a referral channel, with a flat reward for each friend who converts. - Flat credit or cash: Pay a fixed reward per successful referral, as store credit they redeem at checkout or real money on Plus. - Link, code, or first-touch: Track by referral link or code, with first or last-touch attribution, so the right referrer is always credited. ### Events tracked - Referred order or sale - First-touch referral capture - Refund or clawback reversal - Repeat referral from the same customer ### Rewards supported - Flat reward per successful referral - First-touch or last-touch attribution - Store credit or real-money payouts - Bonuses for high-volume referrers ### Scenarios - Reward customers for referrals: Give every customer a referral link and a flat credit when a friend they refer makes a first purchase. - Flat bounty per deal: For higher-value B2B referrals, pay a flat bounty per referred sale with first-touch attribution. - Refunds reverse the reward: If a referred order is refunded inside your window, Siren reverses the referral reward automatically. ### What you'll need - A WordPress site, with WooCommerce, EDD, or an LMS for automatic sale tracking. - The free Lite tier runs link-based referrals. Essentials adds coupon tracking and more reward shapes. - Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is a plugin on your own site. ### Install it from a recipe - Refer-a-Friend Program (Siren Essentials): Customers earn a flat reward for every friend they refer who buys. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program ### Other programs on WordPress - Affiliate program: https://www.sirenaffiliates.com/integrations/wordpress/affiliate-program - Loyalty program: https://www.sirenaffiliates.com/integrations/wordpress/loyalty-program ### Frequently Asked Questions **Is there a free WordPress referral plugin?** Yes. Siren Lite is free and runs link-based referral programs on your WordPress site. Upgrade to Essentials for coupon tracking and more reward shapes, or Plus for automatic real-money payouts. **How is a referral program different from an affiliate program?** Same engine, different audience. A referral program rewards your existing customers for referring friends, usually with a flat credit, while an affiliate program recruits external promoters who earn a commission. Siren runs both on the same site. **Can I reward referrals with store credit instead of cash?** Yes. On Lite and Essentials, referral rewards are issued as store credit that the referrer redeems at checkout. Real-money payouts via Stripe Connect are available on the Plus tier. **Can I run a B2B referral program too?** Yes. For business referrals, the B2B Referral Program recipe pays a flat bounty per referred sale with first-touch attribution, built for partnerships where each conversion carries significant value. **How are referrals tracked on WordPress?** By referral link or assigned code, with first or last-touch attribution. Siren records the referral natively on your site and credits the referrer the moment the referred order completes. ## Run a referral program with Stripe Source: https://www.sirenaffiliates.com/integrations/stripe/referral-program Run a customer referral program with Stripe using Siren Cloud. Reward customers when a friend they refer becomes a paying Stripe subscriber, attributed on the first successful charge and reversed on refunds. Hosted and managed. Reward your customers when a friend they refer becomes a paying Stripe subscriber. Siren Cloud connects to your Stripe account, watches for the first successful charge from a referred friend, and credits the customer who sent them. Here is how it works with Stripe, and the recipe behind it. > Can you run a referral program with Stripe? Yes, with Siren Cloud. Stripe is a payment processor with no built-in referral feature, so Siren connects to your Stripe account over its API and webhooks. When a customer refers a friend and that friend's first Stripe charge succeeds, Siren credits the referring customer. Trials that never convert pay nothing, and credit is reversed automatically on refunds or disputes. ### How it works on Stripe 1. A customer refers a friend: They share a unique referral link from a Siren registration form. No code, no professional affiliate signup. 2. The friend pays on Stripe: The friend subscribes or buys, and their first charge succeeds in your Stripe account. 3. Stripe pushes the event: Stripe sends the charge to Siren Cloud over a webhook, and Siren confirms it via the API within seconds. 4. The customer is rewarded: Siren matches the friend back to the referring customer and credits a reward you can pay out however you like. ### What is different on Stripe - Fires on the first Stripe charge: Attribution starts at the first successful payment. A trial with no charge is not a billable event, so referrals only count when the friend actually pays. - Referral link to Stripe customer: Siren binds the referred friend at signup, then matches the Stripe charge back to the customer who referred them. The newest referral wins if a friend was sent by two people. - Reward tracked, payout your way: Siren tracks the reward as a commission. You pay it as account credit, a discount, cash, or any workflow you run. Stripe Connect can move real money if you want it automated. ### Events tracked - First successful Stripe charge from a referred friend - New subscription created - Recurring invoice paid - Refund or dispute reversal ### Rewards supported - Flat reward per converted friend - Percentage of the friend's first charge - Recurring credit on every renewal - One-time bounty when a trial converts - Tiered by number of friends referred ### Scenarios - Reward customers who refer subscribers: Give every customer a referral link. When a friend they sent becomes a paying Stripe subscriber, the customer earns a reward. Nothing pays out on a trial that never converts. - A simple, fixed reward: Customers think in dollars, not percentages. Pay a flat amount for each friend who pays, whether the friend signs up for the small plan or the large one. - Reversed automatically: If a referred friend's charge is refunded or disputed inside your window, Siren reverses the customer's reward so you never pay on revenue that did not stick. ### What you'll need - A Stripe account you can connect, with read access to charges, subscriptions, and invoices. - A Siren Cloud plan, hosted and set up by our team around your reward model. - A registration form so customers can join and receive their referral link. Siren provides the tools for this. - No engineering lift beyond connecting the account. We handle the setup. ### Install it from a recipe - Refer-a-Friend Program (Cloud): Customers earn a flat reward for every friend they refer who becomes a paying customer. Install from https://www.sirenaffiliates.com/recipes/refer-a-friend-program ### Other programs on Stripe - Affiliate program: https://www.sirenaffiliates.com/integrations/stripe/affiliate-program - Revenue share: https://www.sirenaffiliates.com/integrations/stripe/revenue-share ### Frequently Asked Questions **Does Stripe have a built-in referral program?** No. Stripe is a payment processor, not a referral platform, and it has no feature to track who referred whom or to reward customers for it. Siren Cloud adds that layer by connecting to your Stripe account over its API and turning a referred friend's first successful charge into a tracked, rewardable referral. **When does a customer earn their referral reward?** When the friend they referred actually pays. Siren waits for the first successful Stripe charge from the referred friend before crediting the referring customer. A friend who starts a trial and never converts is not a billable event, so the reward only fires on a real payment. **How does a customer get their referral link?** Through a registration form Siren sets up for your program. Customers join, receive a unique referral link, and share it with friends. When a friend clicks the link, Siren records the referral and binds that friend so their later Stripe charge can be matched back to the right customer. **Can I reward customers on recurring Stripe subscriptions, not just the first payment?** Yes. Siren reads each recurring invoice from Stripe, so you can choose to reward the referring customer once on the first charge or as recurring credit on every renewal for the life of the friend's subscription. Credit is reversed automatically on refunds, disputes, or churn. **Is the Stripe referral program available in the WordPress plugin?** No. Reading Stripe billing as the source of conversions is a Siren Cloud integration. The WordPress edition tracks sales natively on your own WooCommerce site. Siren Cloud is what connects to your Stripe account and runs the referral program for you, hosted and managed. **What stops someone from referring themselves on Stripe?** Siren attributes by the referred friend's Stripe customer, and the reward only pays when a real charge succeeds, which raises the cost of gaming the program. Because every reward is tracked as a commission, you can also review pending rewards before approving payouts if fraud is a concern. ## Run a revenue share on Stripe Source: https://www.sirenaffiliates.com/integrations/stripe/revenue-share Run a revenue share with partners on Stripe using Siren Cloud. Siren connects to your Stripe account, reads every charge and recurring invoice, and pays partners an ongoing percentage of the MRR they bring in. Custom, hosted, and managed. Split a share of Stripe revenue with the partners, resellers, and co-founders who bring it in. Siren Cloud connects to your Stripe account, reads every charge and recurring invoice, and pays an ongoing percentage for the life of each customer. Here is how it works on Stripe, and the recipe that models it. > Can you run a revenue share on Stripe? Yes, with Siren Cloud. Stripe has no native revenue-share or partner-payout feature, so Siren connects to your Stripe account over its API and webhooks, attributes each charge and recurring invoice to the partner who brought the customer in, and pays them an ongoing percentage of that revenue. Attribution can be assigned to a partner directly rather than depending on tracking links, which fits formal partner, reseller, and co-founder splits. Best fit for SaaS and subscription businesses billing on Stripe. ### How it works on Stripe 1. A partner brings a customer: A reseller, co-founder, or strategic partner sends a customer who signs up and starts billing on Stripe. 2. Stripe bills them: A charge succeeds or a recurring invoice is paid, and Stripe pushes the event to Siren Cloud over its webhook and API. 3. Siren attributes the revenue: The customer is tied to the partner, and Siren applies your revenue-share percentage to each qualifying charge and renewal. 4. The share is reconciled: Every period Siren produces a payout statement with the partner's share of the revenue they brought in, renewals included. ### What is different on Stripe - Fires on Stripe billing: One-time charges, new subscriptions, and every recurring invoice, all read natively from your Stripe account so the share keeps paying as long as the customer does. - Assigned, not link-based: Revenue share is for formal relationships, so you tie a customer to a partner directly. There is no tracking link or coupon to depend on, and Siren credits each Stripe charge for that customer automatically. - A reconciled statement: Siren reads Stripe to calculate the split and produces the statement you pay from. It does not move money inside Stripe, so refunds, disputes, and churn reverse the credit automatically. ### Events tracked - One-time charge - Subscription created - Recurring invoice paid - Plan upgrade or expansion - Refund, dispute, or churn reversal ### Rewards supported - Percentage of every charge - Recurring share on each invoice - Tiered by MRR contributed - 50/50 or any custom split - Revenue share with resellers and partners ### Scenarios - Split MRR with a reseller: A partner resells your product and earns an ongoing percentage of the Stripe revenue from every account they bring in, recalculated on each renewal. - Automate a co-founder split: Assign a partner's accounts to them and pay a fixed share of the revenue those accounts generate, for as long as they keep paying on Stripe. - The share stops when they leave: When a Stripe subscription is refunded, disputed, or cancelled, Siren reverses the credit so the revenue share only pays on revenue you actually kept. ### What you'll need - A Stripe account you can connect, with read access to charges, subscriptions, and invoices. - A Siren Cloud plan, hosted and set up by our team around your revenue-share model. - Optionally your CRM or product database, so Siren can attribute by customer rather than by Stripe ID alone. - No engineering lift beyond connecting the account. We handle the setup. ### Install it from a recipe - Business Partner Revenue Share (Cloud): An ongoing revenue share for formal business partnerships, with attribution assigned directly and no tracking links or coupons required. Install from https://www.sirenaffiliates.com/recipes/business-partner-revenue-share ### Other programs on Stripe - Affiliate program: https://www.sirenaffiliates.com/integrations/stripe/affiliate-program - Referral program: https://www.sirenaffiliates.com/integrations/stripe/referral-program ### Frequently Asked Questions **Does Stripe have a revenue-share feature?** No. Stripe is a payment processor, not a partner-payout platform, so there is no built-in way to split a percentage of revenue with the partner who brought a customer in. Siren Cloud adds that layer by connecting to your Stripe account over its API and turning every charge and invoice into an attributable revenue event you can share on. **How does Siren attribute Stripe revenue to a partner for a revenue share?** You tie a customer to the partner who brought them in, rather than relying on a tracking link or coupon. As Stripe charges and recurring invoices arrive for that customer, Siren matches them back to the partner and applies your share. This direct attribution is what makes it suitable for formal reseller, co-founder, and strategic-partner arrangements. **Can the revenue share pay on recurring Stripe subscriptions?** Yes. Siren reads each recurring invoice from Stripe, so a partner earns their share on every renewal for the life of the customer, not just the first charge. Credit is reversed automatically on refunds, disputes, and churn, so the share only ever pays on revenue you keep. **Can different partners earn different revenue-share percentages?** Yes. The share is set per program, so you can run one rate for resellers and another for a co-founder split, or scale the percentage by the MRR a partner contributes. We design the model with you when we connect your Stripe account, so the rates match your actual partnership agreements. **Is the Stripe revenue share available in the WordPress plugin?** No. Reading Stripe billing to run a revenue share is a Siren Cloud integration. The WordPress edition tracks sales on your own WooCommerce site, not in Stripe. The WordPress plugin can use Stripe Connect to pay partners real money, but that is a payout method, not the same as reading Stripe billing as the source of the revenue you share. ## Run a revenue share program on WordPress Source: https://www.sirenaffiliates.com/integrations/wordpress/revenue-share Run a revenue share program on WordPress with Siren. Pay partners, creators, and vendors an ongoing percentage of attributed sales, calculated automatically on your own site. Free to start. Siren is the WordPress plugin for revenue sharing. Pay partners, creators, and vendors an ongoing percentage of the sales they drive or own, calculated automatically on the site you already run. Here is how it works, and the recipe to install it. > Is there a revenue share plugin for WordPress? Yes. Siren is a revenue share plugin for WordPress. Install the Business Partner Revenue Share recipe and partners earn an ongoing percentage of attributed sales, with no tracking links or coupons required. It runs on your own site, on the same engine as your affiliate program. ### How it works on WordPress 1. A partner is assigned: Map a partner, creator, or vendor to the products, categories, or sales they share revenue on. 2. A sale completes: The order completes natively on your WordPress site, with no redirect and no middleman. 3. Siren splits it: Each assigned partner's share is calculated automatically the moment the order completes. 4. Their share is paid: As store credit, or real money via Stripe Connect on Plus. ### What is different on WordPress - By assignment, not links: Revenue share works through manual assignment, so a formal partner earns their percentage without sharing a link or a coupon. - Partners, creators, vendors: Pay a business partner an ongoing cut, a creator a royalty on their content, or a marketplace vendor their share of each sale. - Store credit or real money: Pay each share as store credit, or real money via Stripe Connect on Plus. ### Events tracked - Completed order or sale - Subscription renewal - Specific-product or category sale - Refund or clawback reversal ### Rewards supported - Ongoing percentage of attributed sales - Per-product or per-category splits - Creator and content royalties - Multi-party splits across contributors ### Scenarios - Ongoing cut, no links: Pay a business partner a percentage of attributed sales through manual assignment, with nothing for them to share or track. - Royalty on their content: Pay a creator a percentage whenever the products or courses they made sell on your site. - Split every sale by vendor: Pair with the Marketplace Vendor Commission recipe so every vendor earns their share of each transaction independently. ### What you'll need - A WordPress site, with WooCommerce, EDD, or an LMS for automatic sale tracking. - The Essentials tier for revenue-share assignment and reward shapes. - Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is a plugin on your own site. ### Install it from a recipe - Business Partner Revenue Share (Siren Essentials): Ongoing revenue share for business partners, with no tracking links needed. Install from https://www.sirenaffiliates.com/recipes/business-partner-revenue-share ### Other programs on WordPress - Affiliate program: https://www.sirenaffiliates.com/integrations/wordpress/affiliate-program - Bonus program: https://www.sirenaffiliates.com/integrations/wordpress/bonus-program ### Frequently Asked Questions **Is there a revenue share plugin for WordPress?** Yes. Siren runs revenue share programs on WordPress through manual assignment, so partners earn an ongoing percentage of attributed sales without tracking links or coupon codes. It runs on the same engine as your affiliate, referral, and loyalty programs. **Can partners earn without sharing a link or coupon?** Yes. Revenue share works by assigning a partner to the products, categories, or sales they share in. Their percentage is calculated automatically whenever a qualifying sale completes. **Can I split revenue among multiple partners?** Yes. You can assign more than one partner to a sale, and Siren divides the share between them by your rules. This is how marketplace and multi-contributor splits work. **Does it work for creators and course royalties?** Yes. Pay a creator a percentage whenever their assigned products or courses sell, including LifterLMS and LearnDash course sales, all tracked on the same WordPress site. **How do partners get paid their share?** As store credit on Essentials, or real money through Stripe Connect on the Plus tier. Each partner's share is calculated and recorded for you at payout time. ## Run a royalty program on Easy Digital Downloads Source: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/royalty-program Run a royalty program on Easy Digital Downloads with Siren. Pay creators a percentage every time their assigned downloads sell, tracked by product ownership with no links or codes. Installs from a recipe on Essentials. Pay creators a percentage every time one of their assigned downloads sells, tracked automatically by product ownership rather than referral links or coupon codes. Here is how it works on Easy Digital Downloads, and the recipe to install it. > Can you run a royalty program on Easy Digital Downloads? Yes. Siren is a WordPress plugin that runs natively inside Easy Digital Downloads and pays creators a royalty every time one of their assigned downloads sells. You link each download to its creator with Siren's owned-products feature, and from then on attribution is automatic, with no referral links or coupon codes to share. Royalties are calculated per line item the moment an EDD order completes. Installs from the Product Royalties recipe on the Essentials tier. ### How it works on Easy Digital Downloads 1. A customer buys a download: The order completes natively on your Easy Digital Downloads store. No redirect, no middleman. 2. Siren reads the EDD order: It listens to the order event on the same WordPress site and inspects each line item. 3. It credits the owner: Siren matches each download to the creator who owns it and credits a royalty on that line item's value. 4. The royalty is paid: As Easy Digital Downloads store credit, or real money via Stripe Connect on Plus. ### What is different on Easy Digital Downloads - By product ownership, not links: Royalties are tied to who owns a download, not who shared a link or code. Assign products once and tracking is automatic from then on. - Fires on EDD sales and renewals: Reads completed orders, license and subscription renewals, and refunds natively on the same site. - Each creator earns on their own work: A cart with downloads from three creators generates three separate royalty credits, each based on that download's price. - Store credit or real money: Pay as Easy Digital Downloads store credit on Essentials, or real money via Stripe Connect on the Plus tier. ### Events tracked - Completed Easy Digital Downloads order - License or subscription renewal - Specific-product or bundle sale - Refund or clawback reversal ### Rewards supported - Percentage royalty per product sold - Per-product or per-category rates - Recurring on license and subscription renewals - Independent credit when a product has multiple owners ### Scenarios - Pay vendors a royalty on each sale: List downloads from many vendors and pay each one automatically whenever their product sells, with no links to manage. - Reward the people behind your catalog: Assign each download to its creator and share a defined percentage of every sale, so contributors earn from their own work. - Split royalties on shared products: When a download is owned by more than one creator, each one earns the royalty independently the moment it sells. - Royalties on recurring revenue: With EDD recurring payments, creators can keep earning on license and subscription renewals, not just the first sale. ### What you'll need - A WordPress site with Easy Digital Downloads active. - The Essentials tier, which powers product-ownership tracking and the royalty engagement type. - For royalties on renewals: EDD recurring payments. For automatic real-money payouts: Plus (Stripe Connect). - No referral links, no coupon codes, no API keys. It is the same WordPress site. ### Install it from a recipe - Product Royalties (Essentials): Creators earn a percentage every time their assigned products sell, tracked automatically by product ownership. Install from https://www.sirenaffiliates.com/recipes/product-royalty-program ### Other programs on Easy Digital Downloads - Affiliate program: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/affiliate-program - Referral program: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/referral-program ### Frequently Asked Questions **How do creators get credited for a royalty on Easy Digital Downloads?** By product ownership. You add each creator as a collaborator in Siren and link their downloads to their profile with the owned-products feature. After that, every time one of those downloads sells in Easy Digital Downloads, the creator is credited automatically. There are no links to share and no codes to enter at checkout. **What Siren tier do I need to run a royalty program here?** The Essentials tier. Product-ownership tracking and the royalty engagement type that powers this program are Essentials features, so the free Lite tier is not enough for royalties. The Product Royalties recipe installs the program for you once Essentials is active. **Can one Easy Digital Downloads product belong to more than one creator?** Yes. Assign the same download to each creator who owns it, and when it sells every owner earns the royalty independently. This makes co-authored ebooks, bundled plugin packs, or jointly produced assets straightforward to pay out without any manual splitting. **How is the royalty amount calculated when a cart has products from several creators?** Per line item. Siren reads each line of the completed Easy Digital Downloads order, matches the download to its owner, and credits a percentage of that line's value. A single order containing downloads from three creators produces three separate royalty credits, each reflecting the actual price of that creator's product. **Do royalties apply to license renewals and subscriptions?** Yes, when you are using EDD recurring payments. You can decide whether the creator earns a royalty only on the first sale or on a chosen set of renewals, so creators of subscription software or membership content keep sharing in the recurring revenue their work brings in. **What happens to a royalty if the download is refunded?** Commissions move from pending to payable on your schedule. If an Easy Digital Downloads order is refunded inside your policy window, you can reject that royalty so it is never paid out, which keeps obligations clean for both you and the creator without any manual recalculation. ## Run a royalty program on LearnDash Source: https://www.sirenaffiliates.com/integrations/learndash/royalty-program Run a royalty program on LearnDash with Siren. Assign each course to its instructor, and Siren pays the creator a percentage every time their course sells. No referral links, no manual tracking. Installs from a recipe. Pay your course creators a royalty every time one of their LearnDash courses sells. Assign each course to its instructor once, and Siren credits the right person automatically on every purchase. No referral links, no coupon codes, no manual attribution. Here is how it works on LearnDash, and the recipe to install it. > Can you run a royalty program on LearnDash? Yes. Siren pays course creators a royalty every time one of their LearnDash courses sells, with no referral links or manual tracking. You assign each course to its instructor once using owned products, and Siren credits the owner automatically on every purchase. It runs on the same WordPress site as LearnDash, with course-ownership tracking on the Essentials tier. ### How it works on LearnDash 1. An instructor owns a course: You assign each LearnDash course to its creator once in Siren's owned products settings. 2. A student buys it: The purchase completes natively, whether through LearnDash checkout or WooCommerce on the same site. 3. Siren credits the owner: Siren reads the transaction, sees who owns the course, and applies your royalty rate to that line item. 4. The royalty is paid: As store credit, or as real money via Stripe Connect on the Plus tier. ### What is different on LearnDash - By ownership, not by link: There are no tracking links or coupons. Each course has a clear owner, so the instructor who created it earns the moment it sells. - Fires on the course sale: Siren reads LearnDash transactions natively, whether the student checks out through LearnDash or through WooCommerce on the same site. - Each creator earns independently: Assign a course to more than one instructor and every owner earns their royalty separately on the same sale. - Store credit or real money: Pay royalties as store credit on Essentials, or real money via Stripe Connect on Plus. ### Events tracked - Course purchase (LearnDash or WooCommerce) - Owned-course sale credited to its instructor - Refund reversal (WooCommerce-routed sales) ### Rewards supported - Percentage royalty on each course sale - Per-course rates aligned to each offer - Independent royalties for co-created courses - Royalty plus affiliate commission on the same sale ### Scenarios - A Udemy-style split on WordPress: Assign every course to its creator and pay each instructor a percentage of the sales their courses generate, the standard course-marketplace model rebuilt on your own site. - Two instructors, one course: When a course is built by a team, assign it to each creator. A single sale pays every owner their royalty independently. - Higher royalty on the flagship: Set a different royalty on a premium certification than on an introductory course, so each creator's cut matches the value of their offer. - Reward the referrer and the creator: Run a royalty program alongside an affiliate program. One sale pays the affiliate who referred the student and the instructor who built the course, because they reward different people. ### What you'll need - A WordPress site with LearnDash active. - The Essentials tier, which provides course-ownership tracking and the owned-products engagement type. - Selling through WooCommerce too? Siren tracks both checkouts on the same site. - Plus for automatic real-money payouts via Stripe Connect. ### Install it from a recipe - Course Creator Royalty Program (Essentials): Instructors earn a percentage every time one of their courses sells, tracked automatically by course ownership. Install from https://www.sirenaffiliates.com/recipes/course-creator-royalty-program ### Other programs on LearnDash - Affiliate program: https://www.sirenaffiliates.com/integrations/learndash/affiliate-program ### Frequently Asked Questions **How does Siren know which instructor to pay on LearnDash?** You assign each LearnDash course to its instructor once using Siren's owned products feature. When that course sells, Siren reads the transaction, matches the course to its owner, and credits that instructor with the royalty automatically. There are no referral links or coupon codes involved, because attribution is based on who owns the course, not who referred the sale. **What Siren tier does a LearnDash royalty program need?** The Essentials tier. Course-ownership tracking and the owned-products engagement type that powers instructor royalties are Essentials features. The free Lite tier covers link-based affiliate programs, but ownership-based royalties begin on Essentials. **What happens when a course has more than one instructor?** Assign the course to each instructor in Siren. When the course sells, every owner earns their royalty independently on the same sale. A course co-created by two instructors pays both of them, and each payout is calculated from that course's line item, not the order total. **Can I pay a different royalty rate on different courses?** Yes. You can set per-course royalty rules, so a premium certification can carry a higher rate than an introductory course. Each instructor's payout is based on the price of their specific course, which lets you match royalties to the value and margin of each offer. **Does the royalty program work with both LearnDash checkout and WooCommerce?** Yes. Siren listens to LearnDash transaction events natively, so a course sale is credited whether the student checks out through LearnDash's own payments or through WooCommerce on the same WordPress site. As long as the course is assigned to an instructor in Siren, the royalty is tracked automatically either way. **Can I run a royalty program and an affiliate program at the same time?** Yes. Siren evaluates each program independently against the same transaction. When a student buys a course, the affiliate who referred them can earn a commission and the instructor who created the course can earn a royalty on that one sale, because the two programs reward different people for different contributions. **What happens if a student gets a refund?** You control royalty approval timing. Royalties start in a pending state and can be held until your refund window closes. LearnDash does not expose a refund event to Siren, so those royalties are stopped through this manual review window before payout. If you sell the same courses through WooCommerce, refunds on those orders are reversed automatically, so creators are paid on sales that stick. ## Run a royalty program on WooCommerce Source: https://www.sirenaffiliates.com/integrations/woocommerce/royalty-program Run a royalty program on WooCommerce with Siren. Assign products to creators, vendors, or artists and pay a percentage every time their products sell, tracked automatically by product ownership. No referral links required. Pay creators, vendors, and artists a percentage every time their products sell on your WooCommerce store. Attribution is based on product ownership, not links or codes, so creators earn automatically the moment their item is purchased. Here is how it works on WooCommerce, and the recipe to install it. > Can you run a royalty program on WooCommerce? Yes. Install the Product Royalty Program recipe on a WordPress site running WooCommerce, on the Essentials tier. You assign each product to the creator who owns it, and Siren pays that creator a percentage of every sale of their products, tracked natively the moment the order completes. No referral links or coupon codes are involved. Attribution is based entirely on product ownership. ### How it works on WooCommerce 1. You assign products: Each WooCommerce product is linked to the creator who owns it using Siren's owned products feature. 2. A customer buys: The order completes natively on your WooCommerce store. The creator does not need to share a link or promote anything. 3. Siren attributes it: Siren reads the order line items, identifies the creator who owns each product, and credits each one independently. 4. Royalty is paid: As WooCommerce store credit, or real money via Stripe Connect on Plus. The payout reflects each product's line-item value. ### What is different on WooCommerce - By product ownership, not links: There are no tracking links or coupon codes. You assign products to creators once, and every future sale of those products is credited automatically. - Fires on the product sold event: Siren listens for the collaboratorProductSold engagement on each WooCommerce order, and reverses the royalty if the order is refunded inside your window. - Per-product, per-creator math: A cart with items from three creators produces three separate royalty credits, each calculated on that product's actual line-item price. ### Events tracked - Sale of a creator's assigned WooCommerce product - Sale of a variable or downloadable product - Subscription renewal of an owned product (with Woo Subscriptions) - Refund or clawback reversal ### Rewards supported - Percentage royalty on each product sale - Per-product rates that vary by creator or catalog - Shared ownership so multiple creators earn on one product - Recurring on subscription renewals (with Woo Subscriptions) ### Scenarios - Pay vendors per sale: List products from many vendors and pay each one a royalty on their own sales automatically, with no spreadsheet reconciliation at payout time. - Compensate designers when work sells: Assign each design to its artist. When a customer buys a product carrying that design, the artist earns their royalty on the line item. - Multiple creators on one item: When a product is co-created, assign it to every owner. Each creator earns the royalty independently on the same sale. ### What you'll need - A WordPress site with WooCommerce active. - The Essentials tier. Product ownership tracking and the product-sold engagement type are Essentials features. - Plus for automatic real-money payouts via Stripe Connect. Lower tiers pay store credit or record payouts by hand. - Nothing to integrate and no API keys. It is the same WordPress site as your store. ### Install it from a recipe - Product Royalty Program (Essentials): Creators earn a percentage every time their assigned products sell, tracked automatically by product ownership. Install from https://www.sirenaffiliates.com/recipes/product-royalty-program ### Other programs on WooCommerce - Affiliate program: https://www.sirenaffiliates.com/integrations/woocommerce/affiliate-program - Loyalty program: https://www.sirenaffiliates.com/integrations/woocommerce/loyalty-program - Influencer program: https://www.sirenaffiliates.com/integrations/woocommerce/influencer-program ### Frequently Asked Questions **How are royalties tracked on WooCommerce without referral links?** Attribution is based on product ownership. You assign each WooCommerce product to the creator who owns it in Siren, and from that point every sale of those products is credited automatically. Creators never share a link or code. They supply the product, you list it, and they earn when it sells. **Can one WooCommerce product belong to multiple creators?** Yes. Assign the product to each creator who owns it. When that product sells, every owner earns the royalty independently on the same order, so co-created items split correctly with no manual math. **Does this work for digital downloads and variable products?** Yes. Any WooCommerce product type is supported, including simple, variable, and downloadable products. If WooCommerce can sell it, Siren can track a royalty on it. The royalty is calculated on the product's line-item value at checkout. **What Siren plan do I need for a WooCommerce royalty program?** The Essentials tier. Product ownership tracking and the product-sold engagement type are Essentials features, which is a step above the free Lite tier used for basic affiliate tracking. For automatic real-money payouts, add the Plus tier with Stripe Connect. **What happens if a customer refunds a royalty-bearing order?** Siren reverses the royalty automatically. It reads WooCommerce refunds on the same site, so if a creator's product is refunded inside your clawback window, the credit is reversed and the creator's balance is corrected without manual intervention. **How do creators get paid their royalties?** As WooCommerce store credit on Essentials, or as real money through Stripe Connect on the Plus tier, where creators self-onboard and are paid automatically. Each creator sees their own earnings in a self-serve portal, so they can track royalties without emailing you. ## Run a sales contest on top of Marketron Source: https://www.sirenaffiliates.com/integrations/marketron/sales-contest Run a monthly sales contest or spiff on top of Marketron. Siren Cloud pools a share of billed revenue and pays the top biller automatically, on top of base commission. A monthly contest keeps a radio floor competitive, but tallying it from Marketron by hand is a chore. Siren Cloud pools a share of billed revenue through the month and pays the top-billing rep automatically on the first, layered on top of whatever base commission your reps already earn. > Can I run a monthly sales contest on top of Marketron? Yes, with Siren Cloud. Siren reads billed revenue from Marketron, pools a percentage of it through the month, and pays the highest-billing rep the whole pool on the first. It runs alongside base commission as a separate layer, so a contest or a one-off spiff on a digital push never disturbs the reps' regular pay. ### How it works on Marketron 1. Reps bill through Marketron: Orders are booked and invoiced across the month in Marketron. 2. Siren reads the billing: Marketron's APIs feed each billed order to Siren Cloud as it lands. 3. The pool grows: A set share of qualifying revenue accumulates into the contest pool all month. 4. Top biller wins: On the first, Siren tallies the month and pays the full pool to the leader, then resets. ### What is different on Marketron - A share of the month's billing: The prize is a percentage of qualifying Marketron revenue, so a strong month grows the pool on its own. - Top biller, or top few: The shown setup pays the whole pool to the highest biller. It can also pay places, top three, or split between billing and new-account leaders. The tiebreak is yours to set. - On top of base pay: The contest is a distributor that runs beside your commission programs, so base pay is untouched. ### Events tracked - Order or spot booked - Invoice issued - Payment collected - Monthly reset on the first ### Rewards supported - Winner-takes-all monthly pool - Pool sized as a share of billing - One-off spiff on a specific push - Runs on top of base commission ### Scenarios - Top biller wins the pool: Reps compete on billed revenue all month; the leader takes the whole pool on the first, and the board resets for the next round. - Reward a specific push: Filter the pool to digital or a single daypart for a month to spiff that line, then drop the filter when the push ends. - Base pay stays the same: Because the contest is its own layer, you can start, stop, or resize it without touching the reps' regular commission. ### What you'll need - A Marketron system with API access to billing data. - A Siren Cloud plan, hosted and set up by our team. - Your contest rules: pool percentage, which revenue qualifies, and the cycle. - No development work on your side. You supply the comp rules and roster, and we build the connection onto Marketron's APIs. ### Install it from a recipe - Monthly Sales Bonus (Cloud): A monthly revenue pool the top biller claims in full, layered on top of base commission. Install from https://www.sirenaffiliates.com/recipes/monthly-sales-bonus ### Other programs on Marketron - AE commission: https://www.sirenaffiliates.com/integrations/marketron/ae-commission - New business vs renewal: https://www.sirenaffiliates.com/integrations/marketron/new-business-vs-renewal - Rep splits and override: https://www.sirenaffiliates.com/integrations/marketron/rep-splits-and-override ### Frequently Asked Questions **How is the contest prize calculated?** It is a percentage of qualifying billed revenue from Marketron, pooled through the month. A bigger billing month makes a bigger prize, and you choose the percentage and which orders qualify. **Does the contest change my reps' regular commission?** No. The contest is a separate distributor layered on top of your commission programs. Base pay is calculated exactly as before; the contest just adds a monthly pool for the top performer. **Can I run a short-term spiff instead of a monthly contest?** Yes. Scope the pool to a specific product line, daypart, or push for a single cycle, and Siren pays the spiff to the leader on that filtered revenue. Drop the filter when the push ends. **Can the contest pay top three, or a flat spiff per unit, instead of winner-takes-all?** Yes. Winner-takes-all is the shown default, but the contest can pay places, reward everyone who hits goal, or pay a flat dollar spiff per qualifying unit, for example two hundred and fifty dollars per new digital package. Winner-takes-all is the simplest case, not the only one. **Can I require a rep to hit quota to qualify, and exclude managers and house accounts?** Yes. You can gate the contest on hitting goal, exclude managers or house accounts from the pool, and scope it to one station or the whole group. The qualifier and the scope are set when the cycle opens. **Is the contest scored on billing or collections, and does a prize claw back if the business does not collect?** Your choice. Scored on billing, a prize can be set to true up if a winning rep's order later goes unpaid. Scored on collections, only collected revenue counts toward the standings in the first place. We set which, so a contest payout is not stranded on revenue that never landed. **Can reps see the standings during the month, and can the rules change mid-cycle?** Reps see the live leaderboard and the pool as it grows, so the result is never a surprise on the first. The rules, the pool percentage, the qualifying revenue, and the cycle, are locked when the cycle opens and do not change mid-flight. ## Run account-executive commissions on Marketron Source: https://www.sirenaffiliates.com/integrations/marketron/ae-commission Run tiered AE commissions on top of Marketron. Siren Cloud reads billing and collections from Marketron's Integration Suite APIs and pays a base rate plus a higher rate for top performers, one rate per order. Marketron bills the order; Siren pays the rep. Siren Cloud reads billing and collections from Marketron's Integration Suite APIs and runs your AE comp plan on top: a base rate and a higher rate for top performers, with the commission on every order calculated automatically instead of rebuilt in a spreadsheet every month. > Can you run tiered AE commissions on top of Marketron? Yes, with Siren Cloud. Marketron's own commission report applies a flat rate per rep, so Siren reads its billing and collection events through the Integration Suite APIs and runs two linked rates: a base rate and a higher rate for top performers. Reps start on the base rate, and you promote a rep to the higher rate when they clear your quota. A program group fires exactly one rate per order, and credit reverses before payout when an order goes unpaid. ### How it works on Marketron 1. Marketron bills the order: An order is invoiced or a payment is collected in Marketron. 2. Siren reads the event: Marketron's Integration Suite APIs expose the billing or cash-receipt event to Siren Cloud. 3. Siren applies the rate: The rep's rate fires based on the tier you have them in, base or top performer. 4. Payout: Reconciled into a statement each period, with clawbacks on non-payment. ### What is different on Marketron - Base and top-performer rates: Two linked rates in a program group. A rep earns the base rate, and you promote them to the higher rate when they clear your quota. Only one fires per order. - Billing or collections from Marketron: Pay when an order is invoiced or when the advertiser actually pays. Siren reads both from Marketron's APIs. - Reversed before payout: When the billing system reports an order unpaid, Siren reverses the rep's credit before it is paid out, and on your managed plan we can reinstate it if the advertiser later pays. A made-good spot that re-airs is revenue-neutral, so it does not reverse the credit. ### Events tracked - Order or spot booked - Invoice issued - Payment collected - Non-payment or made-good ### Rewards supported - Base percentage on billing or collections - Higher rate for top performers - Per-daypart or per-product rates - Clawback on non-payment, before payout ### Scenarios - A higher rate for proven reps: Promote a rep to the higher rate when they clear your quota, and Siren pays that rate on their billed orders from then on, with no spreadsheet at month end. - Pay on the event you choose: Pay on invoice or on cash received. Either event comes straight from Marketron, and unpaid invoices reverse the credit. - Different rates by product line: Run a separate rate for digital or a specific daypart alongside the base spot rate, all on the same install. ### What you'll need - A Marketron system with API access to order, billing, and AR data. - A Siren Cloud plan, hosted and set up by our team around your comp plan. - Your real rates: base, the top-performer rate, the bar for promotion, and clawback terms. - No development work on your side. You supply the comp rules and roster, and we build the connection onto Marketron's APIs. ### Install it from a recipe - Tiered Commission Plan (Cloud): Two linked rates, a base and a top-performer rate, with a group rule so only one fires per order. Install from https://www.sirenaffiliates.com/recipes/tiered-affiliate-program ### Other programs on Marketron - New business vs renewal: https://www.sirenaffiliates.com/integrations/marketron/new-business-vs-renewal - Sales contest: https://www.sirenaffiliates.com/integrations/marketron/sales-contest - Rep splits and override: https://www.sirenaffiliates.com/integrations/marketron/rep-splits-and-override ### Frequently Asked Questions **Does Marketron calculate tiered commissions?** Not in tiers. Marketron's commission report applies a flat rate per rep tied to billing or collections, with no base-plus-top-performer structure. Siren reads Marketron's billing events and runs the base and top-performer rates as two linked programs, firing one per order. **Can I pay on collections instead of billing?** Yes. Siren reads both billing and cash-receipt events from Marketron, so you can pay reps when an order is invoiced or when the advertiser pays, and reverse credit automatically on non-payment. **How do the commission tiers work?** A program group holds a base-rate program and a top-performer program. A rep earns the base rate until you promote them to the top-performer tier, which you do when they clear your quota or hit whatever bar you set. The group makes sure only one rate pays per order, even during a promotion. **Can the higher rate kick in automatically when a rep clears quota?** Yes, as part of your managed Cloud plan. The standard setup is tiers you promote reps into; for a hands-off accelerator we build the higher rate to trigger once a rep clears quota, so the promotion happens on its own. It is part of what we build and run for you, not something you wire up yourself. **When a rep moves up a tier, does it apply to the whole period or going forward?** Going forward. The tiers are membership-based, so a rep pays the higher rate on orders from the promotion onward, not retroactively to the start of the period, unless you choose to true that up by hand. This keeps the rate a rep was on at the time of each order unambiguous. **Is quota measured on billing or collections, and over what window?** Siren reads billed and collected revenue from Marketron, so you can see each rep against quota on whatever basis your plan uses, monthly, quarterly, or annual-to-date, gross or net of the agency 15%. Deciding when a rep moves to the top-performer rate is your call from those numbers. Siren runs the tier a rep is in rather than auto-promoting them at a threshold. **Is commission figured on gross or net of the agency 15%?** Whichever your plan intends, and you can set it differently for agency and direct business. Reps are usually paid on net-to-station for agency orders and on the full amount for direct. Siren reads which an order is from Marketron and applies the right base. See the agency-business page for how the 15% is handled. **Can Siren handle a draw against commission?** Yes, as part of your managed Cloud plan. We build a draw against commission into your setup, recoverable or non-recoverable, so a rep on a draw sees the arithmetic they expect. Siren produces the earned-commission math, and the draw handling is part of what we run for you. **Does this replace Marketron?** No. Marketron stays your traffic, billing, and AR system of record. Siren reads its events and calculates what each rep is owed under the plan as written, line by line. What it replaces is the monthly commission spreadsheet. ## Run an affiliate program on Easy Digital Downloads Source: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/affiliate-program Run an affiliate program on Easy Digital Downloads with Siren. Track referred download sales and license renewals by link, coupon, or code, calculate commission the moment an order completes, and install it from a recipe. Free on the WordPress plugin. Pay partners a commission on the Easy Digital Downloads sales they refer, tracked by link, coupon, or code and calculated the moment a download sells. Here is how it works on EDD, and the recipe to install it. > Can you run an affiliate program on Easy Digital Downloads? Yes. Install the Basic Affiliate Program recipe on a WordPress site running Easy Digital Downloads. Siren reads EDD order and license events natively and pays a percentage commission the moment a download sells. There is no external service, and it starts free on Lite. ### How it works on Easy Digital Downloads 1. A partner refers: They share a tracking link, or an assigned coupon code on Essentials. 2. A download sells: The customer buys a download or renews a license natively on your EDD store, with no redirect and no middleman. 3. Siren attributes it: Last-touch by link or code. The right affiliate is credited the moment the EDD order completes. 4. Commission is paid: As store credit redeemable at checkout, or real money via Stripe Connect on Plus. ### What is different on Easy Digital Downloads - Fires on EDD orders and licenses: New download sale, license or subscription renewal, and refund clawback, all read natively on the same WordPress site. - Link, coupon, or manual credit: Give affiliates a tracking link on Lite, or assign a coupon code on Essentials, and every matching EDD checkout is attributed automatically. - Store credit or real money: Pay as EDD store credit on Lite and Essentials, or real money via Stripe Connect on Plus. ### Events tracked - New download or sale on Easy Digital Downloads - License or subscription renewal (with EDD recurring payments) - Refund or clawback reversal - Specific-product or bundle sale ### Rewards supported - Flat or percentage commission - Per-product or per-category rates - Recurring on license and subscription renewals (Essentials, with EDD recurring payments) - Per-download or per-bundle bonuses ### Scenarios - Commission on every renewal: Pay the referring affiliate on the first sale and on a defined set of license renewals, not just the initial purchase. - Track by coupon code: On Essentials, give an influencer a coupon instead of a link, and every EDD checkout that uses it is attributed automatically. - Refunds reverse the commission: If a referred download is refunded inside your window, you can reject the commission so it is never paid out. ### What you'll need - A WordPress site with Easy Digital Downloads active. - The free Lite tier runs the Basic Affiliate Program out of the box. - Essentials for renewal commissions (with EDD recurring payments). Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Basic Affiliate Program (Siren Lite · Free): Simple percentage commission on every referred download sale, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program ### Other programs on Easy Digital Downloads - Referral program: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/referral-program - Royalty program: https://www.sirenaffiliates.com/integrations/easy-digital-downloads/royalty-program ### Frequently Asked Questions **Is the affiliate program free on Easy Digital Downloads?** Yes. The Basic Affiliate Program recipe runs on Siren Lite, which is free. It tracks EDD download sales out of the box. Upgrade later for license renewal commissions, automatic real-money payouts, and team features. **How are affiliates attributed on Easy Digital Downloads?** By tracking link, which is cookie-based and last-touch, or on Essentials by an assigned coupon code that credits whoever it belongs to whenever an EDD checkout uses it. You can also credit a referral manually from the Siren admin. **Can affiliates use coupon codes instead of links?** Yes, on the Essentials tier. Assign a coupon to a collaborator and every Easy Digital Downloads checkout that redeems it is credited to them. This suits creators and influencers who would rather share a memorable code than a tracking URL. **Can I pay commission on license or subscription renewals?** Yes, on Essentials and up with EDD recurring payments. The affiliate earns on the first download sale and on a chosen number of license or subscription renewals afterward, with the partner staying attributed across months. **Can I pay different rates for different downloads?** Absolutely. Siren sets commission rules by product, category, or specific offer, so flagship downloads and bundles can pay more while lower-margin or experimental products pay less, all inside one affiliate program. **How do affiliates get paid on Easy Digital Downloads?** As EDD store credit on Lite and Essentials, which partners redeem at checkout, or as real money through Stripe Connect on the Plus tier. Partners self-onboard Stripe Express and are paid automatically. ## Run an affiliate program on Gravity Forms Source: https://www.sirenaffiliates.com/integrations/gravity-forms/affiliate-program Run an affiliate program on Gravity Forms with Siren. Reward affiliates for the qualified leads and form submissions they refer, attribute each one to the partner who sent it, and install it from a recipe. Pay affiliates for the qualified leads and form submissions they refer through Gravity Forms, attributed to the partner who sent them and counted the moment a connected form is submitted. Here is how it works on Gravity Forms, and the recipe to install it. > Can you run an affiliate program on Gravity Forms? Yes. Siren connects to the Gravity Forms you already have and treats a connected form submission as the conversion event, so affiliates earn for the qualified leads they refer rather than only for closed sales. Each submission is tied to the partner who introduced the visitor, and you pay a flat bounty per qualified lead. It installs from the Pay-Per-Lead Affiliate Program recipe and runs on the Essentials tier. ### How it works on Gravity Forms 1. An affiliate refers: They share their tracking link, sending a visitor to the page that holds your connected Gravity Form. 2. The visitor submits a form: An application, inquiry, or quote request is submitted natively on the same WordPress site, with no redirect. 3. Siren attributes it: First-touch by link. The affiliate who originally introduced the visitor is credited the moment the form is submitted. 4. A bounty is counted: Siren records a qualified lead and adds the flat bounty to that affiliate's balance, ready for payout. ### What is different on Gravity Forms - Fires on form submissions, not orders: A connected Gravity Forms submission is the conversion event. Affiliates earn for leads and applications, even when no sale ever happens. - First-touch, oldest binding wins: Lead-gen credits the affiliate who first introduced the prospect, which rewards finding new audiences over retargeting warm ones. - You decide what counts: Mark which forms and which submissions count as qualified. Junk leads can be reviewed or excluded before you pay. ### Events tracked - Form submission (lead or application) - Qualified paid lead - Partner signup form - Form payment (Gravity Forms and Stripe) ### Rewards supported - Flat bounty per qualified lead - Signup and application bonuses - Tiered by lead volume - Manual review before payout ### Scenarios - Pay per quote or consultation request: Connect your quote or demo request form, give affiliates a link to it, and pay a flat bounty for each qualified submission they send. Sales can close offline later. - Reward trial and free registrations: Treat a registration or trial form as the conversion event, so affiliates earn for the signups they drive even before any revenue lands. - Recruit affiliates with a form: Use an application form as partner onboarding. On submit, Siren creates the affiliate and drops them into the right program automatically. ### What you'll need - A WordPress site with Gravity Forms active. - The forms you want to connect, plus a rule for what counts as a qualified lead. - Essentials runs the Pay-Per-Lead Affiliate Program. Plus adds automatic real-money payouts via Stripe Connect. - Your CRM and email tools stay where they are. Siren just feeds them richer data, like who referred each contact. ### Install it from a recipe - Pay-Per-Lead Affiliate Program (Essentials): A flat bounty for every qualified form submission an affiliate refers, with first-touch attribution. Install from https://www.sirenaffiliates.com/recipes/pay-per-lead-affiliate-program ### Other programs on Gravity Forms - Lead-gen program: https://www.sirenaffiliates.com/integrations/gravity-forms/lead-gen-program ### Frequently Asked Questions **How do affiliates earn on Gravity Forms if there is no sale?** The conversion event is a connected form submission, not an order. When a visitor an affiliate referred submits a qualified form, Siren counts a lead and pays a flat bounty for it. This is what makes a Gravity Forms affiliate program work for service and lead-gen businesses where the money lands offline or later in the funnel. **Why does the first affiliate get the credit instead of the last?** Lead-gen attribution uses first-touch, or oldest binding wins. The affiliate who originally introduced the prospect earns the bounty, even if another affiliate touched the same person afterward. This rewards partners for finding new audiences rather than retargeting visitors who were already on their way in. **How do I control which submissions count as qualified leads?** You choose which Gravity Forms are connected to Siren and which submissions count. Submissions you have not marked qualified do not earn, and a lead can be reviewed or adjusted before payout. Siren gives you the structure to pay against real qualified leads instead of every raw submission. **We also take payment through Gravity Forms and Stripe. Can affiliates earn on those?** Yes. When someone pays through a connected form, Siren records the order, ties it to the referring affiliate, and includes it in payouts alongside the per-lead bounties. So a single Gravity Forms affiliate program can reward both qualified leads and form-based sales. **Can I change the bounty amount later?** Yes. Edit the program's lead bounty in your Siren admin and the new amount applies to all future qualified leads. Commissions already earned on past submissions are not affected. **How do affiliates get paid?** As store credit on Essentials, or as real money through Stripe Connect on the Plus tier. Affiliates self-onboard, see their qualified lead counts, and are paid against them automatically. ## Run an affiliate program on LearnDash Source: https://www.sirenaffiliates.com/integrations/learndash/affiliate-program Run an affiliate program on LearnDash with Siren. Partners refer students, Siren reads the LearnDash purchase event natively, and commission is calculated the moment a course sells. Free to start on Lite. Pay partners a commission on the LearnDash course sales they refer, tracked by referral link and calculated the moment a student enrolls. Siren reads the LearnDash purchase event on the same WordPress site, whether you sell through LearnDash checkout or WooCommerce. Here is how it works, and the recipe to install it. > Can you run an affiliate program on LearnDash? Yes. Install the Course Affiliate Program recipe on a WordPress site running LearnDash. Siren listens to the LearnDash transaction event directly, so it works whether you sell through LearnDash checkout or WooCommerce, and pays a percentage commission on every referred enrollment. There is no external service, and it starts free on Lite. ### How it works on LearnDash 1. A partner refers: They share a tracking link to your course catalog. 2. A student enrolls: The purchase completes natively on your LearnDash site, through LearnDash checkout or WooCommerce, with no redirect and no middleman. 3. Siren attributes it: Last-touch by referral link. The referring affiliate is credited the moment the LearnDash transaction event fires. 4. Commission is paid: As store credit, or real money via Stripe Connect on Plus. ### What is different on LearnDash - Fires on the LearnDash purchase: Siren reads the LearnDash transaction event natively on the same site, so a referred enrollment is attributed instantly whether checkout runs through LearnDash or WooCommerce. - Link or manual credit: Give an affiliate a tracking link to your catalog, and every matching course purchase is attributed automatically. You can also credit a referral by hand when needed. - Optionally reward on finish: Beyond the sale, LearnDash fires course and lesson completion events, so you can layer completion bonuses onto the same partner program if you choose. ### Events tracked - Course purchase (LearnDash or WooCommerce) - Course completion - Lesson completion - Refund reversal (WooCommerce-routed sales) ### Rewards supported - Flat or percentage commission - Per-course rates for high-ticket certifications versus intro courses - Recurring on renewals (WooCommerce-routed sales) - Stacks with instructor royalties on the same sale ### Scenarios - Different rates by course: Pay a higher commission on a flagship certification than on a low-priced intro course, so payouts match the margin on each offer. - Track by referral link: Give an educator or influencer their own tracking link, and every enrollment that comes through it is credited to them automatically. - Affiliate and instructor both earn: When a referred student buys, the affiliate earns their commission and the instructor who built the course earns their royalty, because each program rewards a different person independently. ### What you'll need - A WordPress site with LearnDash active. - Selling through WooCommerce too? Siren tracks both checkouts on the same site. - The free Lite tier runs the Course Affiliate Program out of the box. - Renewal commissions apply to WooCommerce-routed sales. Plus for automatic real-money payouts via Stripe Connect. ### Install it from a recipe - Course Affiliate Program (Siren Lite · Free): A percentage commission on every referred LearnDash course enrollment, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/course-affiliate-program ### Other programs on LearnDash - Royalty program: https://www.sirenaffiliates.com/integrations/learndash/royalty-program ### Frequently Asked Questions **Is the affiliate program free on LearnDash?** Yes. The Course Affiliate Program recipe runs on Siren Lite, which is free. It tracks referred LearnDash enrollments and pays a percentage commission out of the box. Upgrade later for instructor royalties, automatic real-money payouts, and team features. **Does Siren track affiliate sales through LearnDash's own checkout, or only WooCommerce?** Both. Siren listens to the LearnDash transaction event directly on the same WordPress site, so a referred enrollment is attributed whether the student paid through LearnDash's built-in checkout or through WooCommerce. There are no API keys or middleware to configure. **Can I pay different commission rates for different courses?** Yes. You can set per-course commission rules, so a high-ticket certification can carry a higher rate than a low-priced introductory course. This lets affiliate payouts track the real margin on each offer instead of forcing one flat rate across your catalog. **How are affiliates attributed on LearnDash?** By tracking link, which is last-touch and cookie-based. If a student clicks links from two affiliates before enrolling, the most recent referral wins. You can also credit a referral manually when needed. **Can an affiliate program and an instructor royalty program run on the same course sale?** Yes. Siren evaluates each program independently against the same LearnDash transaction. When a referred student enrolls, the affiliate earns their commission and the instructor who created the course earns their royalty, because the two programs reward different people and never compete. **What happens when a referred student gets a refund?** You control commission approval timing. Commissions start pending and can be held until your refund window closes. LearnDash does not expose a refund event to Siren, so refunds on native LearnDash sales are handled through that manual review window before you mark a commission payable. If the same courses are sold through WooCommerce, refunds on those orders are reversed automatically. ## Run an affiliate program on LifterLMS Source: https://www.sirenaffiliates.com/integrations/lifterlms/affiliate-program Run an affiliate program on LifterLMS with Siren. Track referred course and membership enrollments by link, add coupon-code tracking on Essentials, calculate commission the moment a sale completes, and install it from a recipe. Free to start on the WordPress plugin. Pay partners a commission on the LifterLMS course and membership enrollments they refer, tracked by link and calculated the moment a sale completes. Here is how it works on LifterLMS, and the recipe to install it. > Can you run an affiliate program on LifterLMS? Yes. Install the Course Affiliate Program recipe on a WordPress site running LifterLMS. Siren reads each enrollment, course sale, and membership purchase natively on the same site and pays a percentage commission to the referring partner. There is no external service, and it starts free on Lite. ### How it works on LifterLMS 1. A partner refers: They share a tracking link to your course catalog, or an assigned coupon code on Essentials. 2. A student enrolls: The student buys a course or joins a membership natively in LifterLMS, with no redirect and no middleman. 3. Siren attributes it: Newest engagement wins. The right affiliate is credited the moment the LifterLMS transaction completes. 4. Commission is paid: As store credit on Lite and Essentials, or real money via Stripe Connect on Plus. ### What is different on LifterLMS - Fires on LifterLMS enrollments: Course purchase, membership purchase, and membership renewal, all read natively on the same WordPress site as your courses. - Link, manual credit, or coupon: Give affiliates a tracking link to your catalog, credit an enrollment manually, or assign a coupon code on Essentials, and every matching enrollment is attributed automatically. - Reward more than the sale: Beyond the enrollment sale, you can fire rewards on course or lesson completion to align payouts with students who finish. ### Events tracked - Course or membership purchase - Membership renewal (Essentials) - Course completion - Lesson completion ### Rewards supported - Flat or percentage commission - Per-course or per-membership rates - Recurring on membership renewals (Essentials) - Completion-based bonuses (Essentials) ### Scenarios - Commission on every renewal: Pay the referring affiliate on the first membership payment and on a defined set of renewals, not just the join, on Essentials and up. - Track by coupon code: On Essentials, give an instructor or influencer a coupon instead of a link, and every enrollment that uses it is attributed automatically. - Reward students who finish: Tie a bonus to course or lesson completion so payouts follow the outcomes that matter, not only the initial sale. ### What you'll need - A WordPress site with LifterLMS active. - The free Lite tier runs the Course Affiliate Program out of the box. - Essentials for membership renewal and completion-based rewards. Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Course Affiliate Program (Siren Lite · Free): Affiliates earn a percentage commission on every referred course enrollment, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/course-affiliate-program ### Other programs on LifterLMS - Revenue share: https://www.sirenaffiliates.com/integrations/lifterlms/revenue-share ### Frequently Asked Questions **Is the affiliate program free on LifterLMS?** Yes. The Course Affiliate Program recipe runs on Siren Lite, which is free. It tracks referred course and membership enrollments and pays a percentage commission out of the box. Upgrade later for renewals, completion rewards, and automatic payouts. **How are affiliates attributed on LifterLMS?** By tracking link, last-touch and cookie-based, or by an assigned coupon code on Essentials. When a student clicks an affiliate link and then enrolls in a course, Siren credits the most recent referral. You can also credit an enrollment manually. **Can I pay different commission rates for different courses or memberships?** Yes. You can set per-course and per-membership commission rules, so a high-ticket cohort program, a recurring membership, and a low-priced single course can each pay affiliates a different rate. **Can affiliates earn on recurring membership renewals?** Yes, on Essentials and up. If your LifterLMS site sells recurring memberships, you choose whether the affiliate earns on the first payment only or on a defined set of renewals, so partners keep earning as students stay subscribed. **How do affiliates get paid on LifterLMS?** As store credit on Lite and Essentials, or as real money through Stripe Connect on the Plus tier. Partners self-onboard through Stripe Express and are paid automatically once their commissions are approved. **Can I run an affiliate program alongside instructor revenue share on the same site?** Yes. Siren treats them as separate programs, so an affiliate earns a commission for referring the sale while an instructor earns their share for creating the course. The two stack on the same LifterLMS transaction without competing. ## Run an affiliate program on Ninja Forms Source: https://www.sirenaffiliates.com/integrations/ninja-forms/affiliate-program Run an affiliate program on Ninja Forms with Siren. Reward affiliates for the qualified leads and form submissions they refer, attribute each one to the partner who sent it, and install it from a recipe. Pay affiliates for the qualified leads and form submissions they refer through Ninja Forms, attributed to the partner who sent them and counted the moment a connected form is submitted. Here is how it works on Ninja Forms, and the recipe to install it. > Can you run an affiliate program on Ninja Forms? Yes. Siren connects to the Ninja Forms you already have and treats a connected form submission as the conversion event, so affiliates earn for the qualified leads they refer rather than only for closed sales. Each submission is tied to the partner who introduced the visitor, and you pay a flat bounty per qualified lead. It installs from the Pay-Per-Lead Affiliate Program recipe and runs on the Essentials tier. ### How it works on Ninja Forms 1. An affiliate refers: They share their tracking link, sending a visitor to the page that holds your connected Ninja Form. 2. The visitor submits a form: An application, inquiry, or quote request is submitted natively on the same WordPress site, with no redirect. 3. Siren attributes it: First-touch by link. The affiliate who originally introduced the visitor is credited the moment the form is submitted. 4. A bounty is counted: Siren records a qualified lead and adds the flat bounty to that affiliate's balance, ready for payout. ### What is different on Ninja Forms - Fires on form submissions, not orders: A connected Ninja Forms submission is the conversion event. Affiliates earn for leads and applications, even when no sale ever happens. - First-touch, oldest binding wins: Lead-gen credits the affiliate who first introduced the prospect, which rewards finding new audiences over retargeting warm ones. - You decide what counts: Mark which forms and which submissions count as qualified. Junk leads can be reviewed or excluded before you pay. ### Events tracked - Form submission (lead or application) - Qualified paid lead - Partner signup form - Form payment (Stripe or PayPal via Ninja Forms) ### Rewards supported - Flat bounty per qualified lead - Signup and application bonuses - Tiered by lead volume - Manual review before payout ### Scenarios - Pay per quote or consultation request: Connect your quote or demo request form, give affiliates a link to it, and pay a flat bounty for each qualified submission they send. Sales can close offline later. - Reward trial and free registrations: Treat a registration or trial form as the conversion event, so affiliates earn for the signups they drive even before any revenue lands. - Recruit affiliates with a form: Use an application form as partner onboarding. On submit, Siren creates the affiliate and drops them into the right program automatically. ### What you'll need - A WordPress site with Ninja Forms active. - The forms you want to connect, plus a rule for what counts as a qualified lead. - Essentials runs the Pay-Per-Lead Affiliate Program. Plus adds automatic real-money payouts via Stripe Connect. - Your CRM and email tools stay where they are. Siren just feeds them richer data, like who referred each contact. ### Install it from a recipe - Pay-Per-Lead Affiliate Program (Essentials): A flat bounty for every qualified form submission an affiliate refers, with first-touch attribution. Install from https://www.sirenaffiliates.com/recipes/pay-per-lead-affiliate-program ### Other programs on Ninja Forms - Lead-gen program: https://www.sirenaffiliates.com/integrations/ninja-forms/lead-gen-program ### Frequently Asked Questions **How do affiliates earn on Ninja Forms if there is no sale?** The conversion event is a connected form submission, not an order. When a visitor an affiliate referred submits a qualified form, Siren counts a lead and pays a flat bounty for it. This is what makes a Ninja Forms affiliate program work for service and lead-gen businesses where the money lands offline or later in the funnel. **Why does the first affiliate get the credit instead of the last?** Lead-gen attribution uses first-touch, or oldest binding wins. The affiliate who originally introduced the prospect earns the bounty, even if another affiliate touched the same person afterward. This rewards partners for finding new audiences rather than retargeting visitors who were already on their way in. **How do I control which submissions count as qualified leads?** You choose which Ninja Forms are connected to Siren and which submissions count. Submissions you have not marked qualified do not earn, and a lead can be reviewed or adjusted before payout. Siren gives you the structure to pay against real qualified leads instead of every raw submission. **We also take payment through Ninja Forms with Stripe or PayPal. Can affiliates earn on those?** Yes. When someone pays through a connected form, Siren records the order total, ties it to the referring affiliate, and includes it in payouts alongside the per-lead bounties. So a single Ninja Forms affiliate program can reward both qualified leads and form-based sales. **Can I change the bounty amount later?** Yes. Edit the program's lead bounty in your Siren admin and the new amount applies to all future qualified leads. Commissions already earned on past submissions are not affected. **How do affiliates get paid?** As store credit on Essentials, or as real money through Stripe Connect on the Plus tier. Affiliates self-onboard, see their qualified lead counts, and are paid against them automatically. ## Run an affiliate program on North Commerce Source: https://www.sirenaffiliates.com/integrations/north-commerce/affiliate-program Run an affiliate program on North Commerce with Siren. Track referred orders by link, calculate commission the moment a sale completes, and install it from a recipe. Free on the WordPress plugin, with coupon tracking on Essentials. Pay partners a commission on the North Commerce sales they refer, tracked by link and calculated the moment a sale completes. Here is how it works on North Commerce, and the recipe to install it. > Can you run an affiliate program on North Commerce? Yes. Siren is a WordPress plugin that runs natively alongside North Commerce on the same site, reading order events so each referred sale is attributed and a percentage commission is calculated the moment it completes. Install the Basic Affiliate Program recipe, with no external service, and it starts free on Lite. ### How it works on North Commerce 1. A partner refers: They share a tracking link, or an assigned North Commerce coupon code on Essentials. 2. A customer checks out: The order completes natively in North Commerce on the same WordPress site, with no redirect and no middleman. 3. Siren attributes it: Last-touch by link, or by coupon code on Essentials. The right affiliate is credited the instant the order completes. 4. Commission is paid: As store credit, or real money via Stripe Connect on the Plus tier. ### What is different on North Commerce - Fires on North Commerce orders: New sale and refund clawback, both read natively on the same WordPress site. - Link, coupon, or manual credit: Give affiliates a tracking link, or assign a North Commerce coupon on Essentials, and every matching checkout is attributed automatically. - Store credit or real money: Pay as store credit on Lite and Essentials, or real money via Stripe Connect on the Plus tier. ### Events tracked - New North Commerce order or sale - Refund or clawback reversal - Specific-product or category sale ### Rewards supported - Flat or percentage commission - Per-product or per-category rates - Specific-product and category bonuses - Signup and milestone bonuses ### Scenarios - Reward creators on a launch: Give each creator a tracking link or coupon and pay a percentage on the high-AOV sales they drive through North Commerce. - Track by coupon code: On Essentials, assign a North Commerce coupon to an influencer instead of a link, and every checkout that uses it is attributed automatically. - Refunds reverse the commission: If a referred order is refunded inside your policy window, you reject the commission so it is never paid out. ### What you'll need - A WordPress site with North Commerce active. - The free Lite tier runs the Basic Affiliate Program out of the box. - Essentials for coupon-code tracking and the Collaborator Portal. Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Basic Affiliate Program (Siren Lite · Free): Simple percentage commission on every referred sale, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program ### Other programs on North Commerce - Referral program: https://www.sirenaffiliates.com/integrations/north-commerce/referral-program ### Frequently Asked Questions **Is the affiliate program free on North Commerce?** Yes. The Basic Affiliate Program recipe runs on Siren Lite, which is free, and it tracks North Commerce orders out of the box. Upgrade later when you want coupon-code tracking, automatic real-money payouts, or team features. **How are affiliates attributed on North Commerce?** By tracking link, which is cookie-based and last-touch, or by an assigned North Commerce coupon code on the Essentials tier. You can also credit a referral manually from the Siren admin when a partner is added after the sale. **Can affiliates use coupon codes instead of links?** Yes, on the Essentials tier. Assign a North Commerce coupon to a collaborator and every checkout that uses it is credited to them automatically, with no link click required. This suits influencers who would rather share a code than a URL. **Can I pay different commission rates per product?** Yes. You can set rules by product or category, so flagship and high-AOV North Commerce offers carry a different rate than entry-level products in the same program. **What happens if a North Commerce order is refunded?** You control when commissions are approved. If a referred order is refunded inside your policy window, you reject the commission so it is never paid out, which keeps your affiliate spend tied to revenue you actually kept. **How do affiliates get paid?** As store credit on Lite and Essentials, or as real money through Stripe Connect on the Plus tier. Partners self-onboard Stripe Express and are paid automatically once you approve their commissions. ## Run an affiliate program on WooCommerce Source: https://www.sirenaffiliates.com/integrations/woocommerce/affiliate-program Run an affiliate program on WooCommerce with Siren. Track referred orders by link, coupon, or code, calculate commission the moment an order completes, and install it from a recipe. Free on the WordPress plugin. Pay partners a commission on the WooCommerce sales they refer, tracked by link, coupon, or code and calculated the moment an order completes. Here is how it works on WooCommerce, and the recipe to install it. > Can you run an affiliate program on WooCommerce? Yes. Install the Basic Affiliate Program recipe on a WordPress site running WooCommerce. Siren tracks each referred order natively and pays a percentage commission. There is no external service, and it starts free on Lite. ### How it works on WooCommerce 1. A partner refers: They share a tracking link, or an assigned coupon code on Essentials. 2. A customer buys: The order completes natively on your WooCommerce store, with no redirect and no middleman. 3. Siren attributes it: Last-touch by link or code. The right affiliate is credited the moment the order completes. 4. Commission is paid: As WooCommerce store credit, or real money via Stripe Connect on Plus. ### What is different on WooCommerce - Fires on WooCommerce orders: Order completed, subscription renewal, and refund clawback, all read natively on the same site. - Link, coupon, or manual credit: Give affiliates a tracking link on Lite, or assign a coupon code on Essentials, and every matching checkout is attributed automatically. - Store credit or real money: Pay as WooCommerce store credit on Lite and Essentials, or real money via Stripe Connect on Plus. ### Events tracked - Completed WooCommerce order - Subscription renewal (with Woo Subscriptions) - Refund or clawback reversal - Specific-product or category sale ### Rewards supported - Flat or percentage commission - Per-product or per-category rates - Recurring on subscription renewals (Essentials, with Woo Subscriptions) - Signup and first-order bonuses ### Scenarios - Commission on every renewal: Pay the referring affiliate on the first order and each WooCommerce Subscriptions renewal, not just month one. - Track by coupon code: On Essentials, give an influencer a coupon instead of a link, and every checkout that uses it is attributed automatically. - Refunds reverse the commission: If a referred order is refunded inside your window, Siren reverses the commission automatically. ### What you'll need - A WordPress site with WooCommerce active. - The free Lite tier runs the Basic Affiliate Program out of the box. - Essentials for renewal commissions. Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Basic Affiliate Program (Siren Lite · Free): Simple percentage commission on every referred sale, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program ### Other programs on WooCommerce - Loyalty program: https://www.sirenaffiliates.com/integrations/woocommerce/loyalty-program - Influencer program: https://www.sirenaffiliates.com/integrations/woocommerce/influencer-program - Royalty program: https://www.sirenaffiliates.com/integrations/woocommerce/royalty-program ### Frequently Asked Questions **Is the affiliate program free on WooCommerce?** Yes. The Basic Affiliate Program recipe runs on Siren Lite, which is free. Upgrade later for renewals, automatic payouts, and team features. **How are affiliates attributed on WooCommerce?** By tracking link, cookie-based and last-touch, or by an assigned coupon code on Essentials. You can also credit a referral manually. **Can affiliates use coupon codes instead of links?** Yes, on the Essentials tier. Assign a coupon to a collaborator and every checkout that uses it is credited to them. This is useful for influencers who would rather share a code. **Can I pay commission on subscription renewals?** Yes, on Essentials and up with WooCommerce Subscriptions. The affiliate earns on the first order and each renewal. **How do affiliates get paid?** As WooCommerce store credit on Lite and Essentials, or as real money through Stripe Connect on the Plus tier. Partners self-onboard and are paid automatically. ## Run an affiliate program on WordPress Source: https://www.sirenaffiliates.com/integrations/wordpress/affiliate-program Run an affiliate program on WordPress with Siren, the plugin built for real partnership programs. Track referred sales by link, coupon, or code, calculate commission natively, and install it from a recipe. Free to start on Lite. Siren is the WordPress plugin for real affiliate programs. Pay partners a commission on the sales they refer, tracked by link, coupon, or code and calculated the moment an order completes, all on the site you already own. Here is how it works, and the recipe to install it. > Is there a WordPress plugin to run an affiliate program? Yes. Siren is a WordPress affiliate plugin. Install the Basic Affiliate Program recipe on your site, and Siren tracks each referred sale natively and pays a percentage commission. There is no external service, and it starts free on Lite. ### How it works on WordPress 1. A partner refers: They share a tracking link, or an assigned coupon code on Essentials. 2. A customer buys: The sale completes natively on your WordPress site, with no redirect and no middleman. 3. Siren attributes it: Last-touch by link or code. The right affiliate is credited the moment the order completes. 4. Commission is paid: As store credit, or real money via Stripe Connect on Plus. ### What is different on WordPress - On the site you own: Siren is a plugin on your own WordPress install, so your affiliates, links, and payout history live in your database, not a third party's. - Link, coupon, or manual credit: Give affiliates a tracking link on Lite, assign a coupon on Essentials, or credit a referral by hand. Every matching sale is attributed automatically. - Store credit or real money: Pay as store credit on Lite and Essentials, or real money via Stripe Connect on Plus. ### Events tracked - Completed order or sale - Subscription renewal - Refund or clawback reversal - Specific-product or category sale ### Rewards supported - Flat or percentage commission - Per-product or per-category rates - Recurring on subscription renewals (Essentials) - Signup and first-order bonuses ### Scenarios - WooCommerce, EDD, and more: Siren tracks affiliate sales across WooCommerce, Easy Digital Downloads, and your LMS, because it runs on the same site they do. - Track by coupon code: On Essentials, give an influencer a coupon instead of a link, and every checkout that uses it is attributed automatically. - Refunds reverse the commission: If a referred order is refunded inside your window, Siren reverses the commission automatically. ### What you'll need - A WordPress site, with WooCommerce, EDD, or an LMS for automatic sale tracking. - The free Lite tier runs the Basic Affiliate Program out of the box. - Essentials for coupon and renewal commissions. Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is a plugin on your own site. ### Install it from a recipe - Basic Affiliate Program (Siren Lite · Free): Simple percentage commission on every referred sale, with automatic attribution. Install from https://www.sirenaffiliates.com/recipes/basic-affiliate-program ### Other programs on WordPress - Revenue share: https://www.sirenaffiliates.com/integrations/wordpress/revenue-share - Bonus program: https://www.sirenaffiliates.com/integrations/wordpress/bonus-program ### Frequently Asked Questions **Is there a free WordPress affiliate plugin?** Yes. The Basic Affiliate Program recipe runs on Siren Lite, which is free: unlimited programs and partners, link and manual tracking, and flat or percentage commissions. Upgrade later for coupons, renewals, automatic payouts, and team features. **How are affiliates attributed on WordPress?** By tracking link, cookie-based and last-touch, or by an assigned coupon code on Essentials. You can also credit a referral manually from the admin. **Can affiliates use coupon codes instead of links?** Yes, on the Essentials tier. Assign a coupon to a collaborator and every checkout that uses it is credited to them. This is useful for influencers who would rather share a code. **Does it work with any WordPress store or just WooCommerce?** Siren tracks WooCommerce and Easy Digital Downloads natively, plus LifterLMS and LearnDash course sales and Gravity Forms leads. Any sale your WordPress site records, Siren can attribute and reward. **How do affiliates get paid?** As store credit on Lite and Essentials, or as real money through Stripe Connect on the Plus tier. Partners self-onboard and are paid automatically. ## Run an affiliate program with Stripe Source: https://www.sirenaffiliates.com/integrations/stripe/affiliate-program Run an affiliate program with Stripe. Siren Cloud connects to your Stripe account, attributes every charge, subscription, and recurring invoice to the partner who referred it, and pays on renewals, not just the first sale. Stripe processes the payment. Siren attributes it. Siren Cloud connects to your Stripe account over its API and webhooks, ties each charge, new subscription, and recurring invoice to the partner who referred the customer, and produces a reconciled payout statement every period. Built for SaaS and any business that bills on Stripe. > Can you run an affiliate program with Stripe? Yes, with Siren Cloud. Stripe is a payment processor and ships no affiliate feature, so Siren connects to your Stripe account over its API and webhooks, attributing each charge, new subscription, and recurring invoice to the partner who referred the customer. Commission accrues on every renewal for the life of the subscription, and credit reverses automatically on refunds, disputes, and churn. Best fit for SaaS and subscription businesses billing on Stripe. ### How it works on Stripe 1. A partner refers: They share a tracking link, and Siren records the referral against the visitor who later becomes a Stripe customer. 2. Stripe bills the customer: A charge succeeds, a subscription starts, or a recurring invoice is paid in your Stripe account. 3. Stripe pushes the event: A webhook fires to Siren Cloud, and Siren confirms the charge or invoice over the Stripe API within seconds. 4. Siren attributes and reconciles: It matches the customer to the referring partner, applies your comp rules, and adds the commission to that period's payout statement. ### What is different on Stripe - Fires on Stripe billing: One-time charge, subscription created, recurring invoice paid, plan upgrade, and refund or dispute reversal, all read straight from your Stripe account. - Link to customer, then customer to charge: Siren ties the referral to the customer at signup. Every Stripe charge and invoice for that customer is matched back to the original partner automatically. - A reconciled statement, not a money move: Siren produces the payout statement you pay from each period. It can also pay partners real money through Stripe Connect. Siren does not move funds inside your Stripe balance. ### Events tracked - One-time charge - Subscription created - Recurring invoice paid - Plan upgrade or expansion - Refund or dispute reversal ### Rewards supported - Flat or percentage of each charge - Recurring commission on every invoice - One-time bounty when a trial converts - Tiered by MRR contributed - Pay partners via Stripe Connect ### Scenarios - Commission on every renewal: Siren reads each recurring Stripe invoice, so a partner earns for as long as the customer stays subscribed, not just on month one. The credit stops automatically when the subscription churns. - Pay only when a trial converts: Wait for the first successful Stripe charge, then pay a flat bounty. Trials that never convert to a payment are not billable events, so nothing is owed on them. - Reward partners on upgrades: When a Stripe subscription moves to a higher plan, Siren can pay on the expansion, rewarding the partner for the larger account, not just the original sale. ### What you'll need - A Stripe account you can connect, with read access to charges, subscriptions, and invoices. - A Siren Cloud plan, hosted and set up by our team around your comp model. - Optionally your CRM or product database, so Siren can attribute by customer rather than by raw Stripe ID. - No engineering lift beyond connecting the account. We handle the setup. ### Install it from a recipe - Tiered Affiliate Program (Cloud): Standard and VIP commission tiers with automatic attribution, so top partners earn a higher rate. Install from https://www.sirenaffiliates.com/recipes/tiered-affiliate-program ### Other programs on Stripe - Revenue share: https://www.sirenaffiliates.com/integrations/stripe/revenue-share - Referral program: https://www.sirenaffiliates.com/integrations/stripe/referral-program ### Frequently Asked Questions **Does Stripe have a built-in affiliate program?** No. Stripe is a payment processor, not an affiliate platform, and it has no referral or commission feature. Siren Cloud adds that layer by connecting to your Stripe account over its API and turning billing events into tracked, attributable referrals that you can pay partners on. **Can I pay affiliates on recurring Stripe subscriptions?** Yes. Siren reads each recurring invoice from Stripe, so partners earn on every renewal for the life of the customer, not just the first charge. If a customer is refunded, disputes a charge, or churns, the matching commission reverses automatically. **How does Siren attribute a Stripe charge to a partner?** When a referred visitor signs up, Siren records the referral and ties it to the customer. As Stripe charges and invoices arrive for that customer, Siren matches them back to the original partner and applies your commission rules. Attribution starts at the first successful payment. **Is the Stripe affiliate integration available in the WordPress plugin?** No. Tracking Stripe billing as your source of sales is a Siren Cloud integration. The WordPress edition reads orders from WooCommerce on your own site instead. Separately, the WordPress edition can use Stripe Connect to pay affiliates real money, which is a different thing from reading Stripe billing to attribute referrals. **Does Siren move money inside my Stripe account?** No. Siren reads Stripe events to calculate what each partner is owed and produces a reconciled payout statement every period. It does not touch your Stripe balance or charge a per-transaction fee. You pay partners from the statement, optionally through Stripe Connect. **What does the affiliate program cost on Stripe?** Siren Cloud is custom-priced: a one-time setup to connect Stripe and design your comp model, plus an ongoing managed subscription. There is no fee skimmed from each Stripe charge. Tell us how you bill and we will scope real numbers for your program. ## Run an influencer program on WooCommerce Source: https://www.sirenaffiliates.com/integrations/woocommerce/influencer-program Run an influencer program on WooCommerce with Siren. Give each creator a unique coupon code, credit every checkout that uses it, and pay a percentage commission the moment the order completes. Coupon tracking is on the Essentials tier. Give each creator a coupon code instead of a link, then pay a commission on every WooCommerce sale that code drives. Codes live in Instagram bios, TikTok captions, and YouTube descriptions, where clickable affiliate links are impractical. Here is how it works on WooCommerce, and the recipe to install it. > Can you run an influencer program on WooCommerce? Yes. Install the Coupon-Based Influencer Program recipe on a WordPress site running WooCommerce. Assign each creator a unique WooCommerce coupon code, and Siren credits every checkout that uses it, no referral link required. It pays a percentage commission the moment the order completes. Coupon-code tracking is an Essentials feature. ### How it works on WooCommerce 1. A creator shares a code: They post their assigned WooCommerce coupon code in a bio, story, caption, or video description. 2. A fan checks out: The shopper enters the code at checkout on your WooCommerce store. The code can give a discount or be zero-dollar and used purely for tracking. 3. Siren attributes it: The bound coupon ties the order to the right creator automatically. If two codes appear on one order, the newest entry wins. 4. Commission is paid: A percentage of the line-item total, as WooCommerce store credit or real money via Stripe Connect on Plus. ### What is different on WooCommerce - Coupon codes, not links: Bind a WooCommerce coupon to a creator and every checkout that uses it is credited. No cookies, no tracking links, ideal for social bios and captions. - Fires on coupon use: Siren listens for a bound coupon at WooCommerce checkout, then reads the completed order natively on the same site. - One creator, many campaigns: Assign several WooCommerce coupons to a single creator to run different codes per platform or campaign, all crediting the same collaborator. ### Events tracked - Bound coupon used at WooCommerce checkout - Completed WooCommerce order - Subscription renewal (with Woo Subscriptions) - Refund or clawback reversal ### Rewards supported - Flat or percentage commission on coupon-driven sales - Per-product or per-category rates - Recurring on subscription renewals (Essentials, with Woo Subscriptions) - Contest and milestone bonuses for top creators ### Scenarios - One code per platform: Give a creator separate codes for Instagram, TikTok, and YouTube, all bound to one collaborator, so you can see which channel sells. - Zero-dollar codes: Use a coupon with no discount purely to attribute the sale, so creators can promote without cutting into your margin. - Run alongside affiliates: Coupon tracking runs independently of a link-based program, so a single order can credit both a coupon code and a referral link. ### What you'll need - A WordPress site with WooCommerce active. - The Essentials tier runs the Coupon-Based Influencer Program recipe, since coupon-code tracking is an Essentials feature. - Essentials also adds renewal commissions on Woo Subscriptions. Plus for automatic real-money payouts via Stripe Connect. - Nothing to integrate and no API keys. It is the same WordPress site. ### Install it from a recipe - Coupon-Based Influencer Program (Siren Essentials): Track influencer commissions through unique coupon codes instead of links, with automatic attribution at checkout. Install from https://www.sirenaffiliates.com/recipes/coupon-based-influencer-program ### Other programs on WooCommerce - Affiliate program: https://www.sirenaffiliates.com/integrations/woocommerce/affiliate-program - Loyalty program: https://www.sirenaffiliates.com/integrations/woocommerce/loyalty-program - Royalty program: https://www.sirenaffiliates.com/integrations/woocommerce/royalty-program ### Frequently Asked Questions **How do creators get tracked without a referral link?** By coupon code. You create a WooCommerce coupon, bind it to a creator's collaborator profile in Siren, and every checkout that applies that code is credited to them automatically. Links are optional, which is why this program suits creators who share codes in bios and captions rather than clickable links. **Can one influencer have more than one coupon code?** Yes. You can assign several WooCommerce coupons to a single collaborator. Many brands give a creator a different code per platform or per campaign so they can see which channel drives sales, while all of those codes credit the same person. **Does the coupon have to give the customer a discount?** No. You can set a WooCommerce coupon to a zero-dollar discount and use it purely for tracking. The code attributes the sale without reducing the order total, which lets creators promote without cutting into your margin. **What does the influencer program cost on WooCommerce?** The Coupon-Based Influencer Program runs on the Essentials tier, because coupon-code tracking is an Essentials feature. Essentials also covers renewal commissions, and you can move to Plus for automatic real-money payouts when your program grows. **What happens if a customer uses two influencer codes on one order?** Siren credits the most recently entered code, so the newest binding wins. In practice WooCommerce usually allows only one coupon per order, so this conflict is rare. A coupon program also runs independently of any link-based affiliate program, so both can fire on the same transaction. ## Run rep splits and manager overrides on Marketron Source: https://www.sirenaffiliates.com/integrations/marketron/rep-splits-and-override Split a deal between co-selling reps and pay the sales manager a team override, all from Marketron billing. Siren Cloud shares the deal and pools the override monthly. When two reps share an account, or a sales manager earns on the whole team, Marketron's flat commission report leaves you reconciling it by hand. Siren Cloud reads billing from Marketron, splits a shared deal between the reps on it, and pays the manager a monthly override on total team revenue, all on the same install. > Can Siren split a deal between reps and pay a manager override from Marketron? Yes, with Siren Cloud. Siren reads billed orders from Marketron and divides a shared deal between the reps credited on it, evenly or as you set. Separately, a monthly override distributor pools a percentage of total team billing and pays the sales manager. The two read the same Marketron data but pay independently, so the override is never taken out of the reps' commission. ### How it works on Marketron 1. The team bills through Marketron: Reps book and invoice orders, some shared between two reps, across the month. 2. Siren reads the billing: Marketron's APIs feed each billed order to Siren Cloud as it lands. 3. Shared deals split: An order credited to two reps divides between them; a solo order pays one rep in full. 4. Manager override pays: A monthly pool of team revenue pays the manager on the first, separate from rep pay. ### What is different on Marketron - Co-sold deals divide: When two reps share an account, the deal's commission splits between them; a single rep on a deal takes it whole. - Manager earns on the team: A separate monthly distributor pools a percentage of total team billing and pays the sales manager. - The override is not a cut: The override is funded from its own revenue share, not deducted from the reps, so both payouts stand on their own. ### Events tracked - Order or spot booked - Invoice issued - Payment collected - Monthly override on the first ### Rewards supported - Even or weighted split between reps - Monthly manager override on team revenue - Multiple managers share the override pool - Clawback on non-payment and make-goods ### Scenarios - Two reps, one deal: When two reps share an advertiser, Siren splits the deal between them automatically instead of leaving the division to a month-end conversation. - Pay leadership on the team: The sales manager earns a percentage of everything the team bills, pooled through the month and paid on the first. - Split the override too: Co-managers or a regional structure can share the override pool evenly, all from the same Marketron billing. ### What you'll need - A Marketron system with API access to billing data. - A Siren Cloud plan, hosted and set up by our team. - Your split rule, the manager override percentage, and the override cycle. - No development work on your side. You supply the comp rules and roster, and we build the connection onto Marketron's APIs. ### Install it from a recipe - Rep Split and Manager Override (Cloud): A shared program that splits a deal between reps, plus a distributor that pays the manager a monthly team override. Install from https://www.sirenaffiliates.com/recipes/rep-split-and-manager-override ### Other programs on Marketron - AE commission: https://www.sirenaffiliates.com/integrations/marketron/ae-commission - Sales contest: https://www.sirenaffiliates.com/integrations/marketron/sales-contest - New business vs renewal: https://www.sirenaffiliates.com/integrations/marketron/new-business-vs-renewal ### Frequently Asked Questions **How does a shared deal split between reps?** Both reps are credited on the shared account, and Siren divides the deal's commission between them, evenly by default or weighted if you prefer. When only one rep is on a deal, that rep earns the full commission, so solo deals need no special handling. **Does the manager override come out of the reps' commission?** No. The reps earn their split on the deal, and the manager's override is funded separately from a percentage of total team revenue. They are two independent payouts reading the same Marketron billing, not one pool the manager takes a cut of. **When does the manager override pay?** On a monthly cycle. The override distributor pools a set percentage of qualifying team billing through the month and pays the manager on the first of the next month, then resets. The percentage and cycle are configurable. **Is the override on gross or net, and on billing or collections?** Whatever your plan says, and you set it explicitly. The override pools a percentage of team revenue, and you choose the base: gross billing or net-to-station, billed or collected, agency business included or not. We set this at the start so the single most scrutinized line in your plan is not left to interpretation. **If a rep's deal goes unpaid after I am paid my override, does the override reverse?** Yes, if you pool on billing. A later non-payment reduces the pool, so the override trues up rather than paying on revenue that never landed. If you pool on collections instead, the override only ever counts money already received. Either way the base is consistent with how your reps are clawed back. **Where is the split percentage set, and can it differ per account?** In Siren, per account or per deal, so a 60/40 split on one account and 50/50 on another both hold. Marketron does not always carry a clean two-rep split, so Siren is where the weight lives, and each rep sees their own percentage on their statement and can flag it before payout. The recipe ships an even split as the starting point; weighting is a setup option. **Our sales manager also carries a list. Does the override include their own sales?** Your call. A player-coach manager can earn rep commission on their personal accounts and an override on the team, or you can exclude their own sales from the override base to avoid double-counting. This is common at a group our size and it is a setup choice, not a fixed rule. **Can the override be conditional on the team hitting its number?** Yes. The override can be flat, or it can switch on only when the team clears goal, or step up past it. Tell us the threshold and Siren applies it to the pooled team revenue each cycle. **Can more than one manager earn the override?** Yes. Enroll each manager on the override and the monthly pool divides evenly between them, which fits co-managers or a regional structure where several leaders share it. ## Run revenue share on LifterLMS Source: https://www.sirenaffiliates.com/integrations/lifterlms/revenue-share Run instructor revenue share on LifterLMS with Siren. Pool a percentage of monthly course revenue and split it among instructors by the lesson and course completions their content earns. Installs from a recipe. Pool a share of your LifterLMS revenue each period and split it among the instructors who teach, weighted by the lesson and course completions their content earns. Here is how it works on LifterLMS, and the recipe to install it. > Can you run instructor revenue share on LifterLMS? Yes. Siren is a WordPress plugin that runs natively inside LifterLMS, so it reads transaction and completion events on the same site. A distributor pools a percentage of revenue each period and splits it among instructors by the engagement their courses generate, then distributes on the schedule you set. Revenue share runs on the Essentials tier. ### How it works on LifterLMS 1. Students learn: They enroll, pay, and work through courses and lessons on your LifterLMS site. 2. Siren scores engagement: Each lesson and course completion is attributed to the bound instructor and weighted into their score. 3. The pool is split: A set percentage of qualifying revenue forms a pool, divided in proportion to each instructor's share of total completions. 4. Instructors are paid: On the schedule you set, as store credit or real money via Stripe Connect on Plus. ### What is different on LifterLMS - Completions, not just sales: Siren reads LifterLMS course and lesson completions natively, so the split tracks the engagement instructors earn, not only the moment a course sold. - A percentage of real revenue: Filter the pool to subscription and membership income so one-time sales and unrelated revenue stay out of the instructor split. - Instructors bound to courses: Add each instructor as a collaborator and bind them to the courses they teach. Completions credit the right person automatically. ### Events tracked - Course completion - Lesson completion - Course or membership purchase - Membership renewal ### Rewards supported - Instructor revenue share - Performance-weighted pool by completions - Course completion weighted above lesson completion - Scheduled, recurring distribution ### Scenarios - Pay your best instructors more: On a multi-instructor LifterLMS site, the instructors whose courses students actually finish take a larger slice of the monthly pool. - Split subscription revenue: Pool a percentage of recurring membership income each period and divide it among instructors by the completions their courses drove. - Reward content that performs: A course published mid-period starts earning engagement points the moment it is bound, so its instructor's share adjusts on its own. ### What you'll need - A WordPress site with LifterLMS active. - The Essentials tier, which runs the Distributor feature behind revenue share. - Instructors added as collaborators and bound to the courses they teach. - Plus for automatic real-money payouts via Stripe Connect. ### Install it from a recipe - Instructor Revenue Share (Essentials): Split a percentage of monthly revenue among instructors by the student engagement their courses earn. Install from https://www.sirenaffiliates.com/recipes/instructor-revenue-share ### Other programs on LifterLMS - Affiliate program: https://www.sirenaffiliates.com/integrations/lifterlms/affiliate-program ### Frequently Asked Questions **How is each instructor's share of the LifterLMS pool decided?** By the completions their courses generate. Siren scores every lesson and course completion attributed to a bound instructor, then splits the pool in proportion to each instructor's score. An instructor whose courses drove 40 percent of tracked completions earns 40 percent of the pool. **Why weight course completions above lesson completions?** Lesson completions reward steady engagement throughout a course, while a full course completion is the outcome you most want to pay for. The recommended weighting counts a course completion ten times a single lesson completion, so instructors whose students finish are paid the most. **Can I limit the revenue share pool to subscription income only?** Yes. Configure the pool filters to include only subscription and membership transactions. One-time course sales and any other LifterLMS revenue then stay outside the instructor split, so you control exactly what is shared. **How are instructors connected to their courses?** Each instructor is added as a collaborator in Siren and bound to the courses they teach. When a student completes a lesson or course on your LifterLMS site, Siren attributes that engagement to the bound instructor automatically. No manual tracking each period. **How and when do instructors get paid?** On the schedule you set for the distributor, such as the first of each month. Payouts go out as store credit, or as real money through Stripe Connect on the Plus tier, where instructors self-onboard and are paid automatically. **Can I run revenue share alongside an affiliate program on the same site?** Yes. Siren lets you define separate programs, so you can pay affiliates a commission for the sales they refer and pay instructors a share of revenue for the courses they create, both on the same LifterLMS site.