Siren

Transactions

Financial transaction records in Siren — data model, three-layer PHP structure, status lifecycle, REST API, and PHP data access.

Last updated: April 9, 2026

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 in the attribution pipeline: when a transaction is attributed to a collaborator, it produces an engagement and conversion, which in turn triggers an obligation 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

FieldREST namePHP getterTypeDescription
IDidgetId()integerUnique identifier
StatusstatusgetStatus()stringCurrent status: complete, cancelled, or refunded
CreateddateCreatedgetCreatedDate()datetimeWhen 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.

FieldREST namePHP getterTypeDescription
IDidgetId()integerUnique identifier
TransactiontransactionIdgetTransactionId()integerThe parent transaction this detail belongs to
NamenamegetName()stringDisplay name (e.g., a product name)
DescriptiondescriptiongetDescription()stringLonger description of the line item
TypetypegetType()stringType identifier (see detail types below)
ValuevaluegetValue()integer (REST) / Amount (PHP)Per-unit value in the smallest currency unit (e.g., cents)
QuantityquantitygetQuantity()integerNumber of units
UnitsunitsgetUnits()stringCurrency 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 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

TypeDescription
productA product line item (the main purchasable goods)
subscriptionA subscription product line item
shippingShipping charges
taxTax amounts
discountDiscount amounts (stored as negative values)
feeAdditional fees (payment processing, handling, etc.)

These types correspond directly to the built-in 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 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.

FieldPHP getterTypeDescription
Transaction Detail IDgetTransactionDetailId()integerThe transaction detail this attribute belongs to
KeygetKey()stringThe attribute key (e.g., sku, categories, collaborators)
ValuegetValue()mixedThe 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:

KeyValue formatDescription
categoriesJSON array of stringsThe product’s category names or IDs
skustringThe product’s SKU
collaboratorsJSON array of intsCollaborator IDs associated with (“owning”) this product
externalIdstringThe 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.

FieldTypeDescription
idstringUnique source type identifier (e.g., wc_order)
namestringHuman-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.

FieldTypeDescription
detailsobject[]Array of transaction detail objects
detailCountintegerNumber of detail line items on the transaction
totalValueintegerSum of all detail values, in the smallest currency unit (e.g., cents)
totalQuantityintegerSum of all detail quantities
currencystring or nullISO 4217 currency code from the first detail line item, or null if no details exist
sourcestring or nullExternal source type identifier from the mapping system (e.g., wc_order)
sourceIdstring or nullExternal source record ID from the mapping system

Status lifecycle

StatusDescription
completeDefault state. The transaction represents a successful commerce event.
cancelledThe transaction has been voided. Downstream conversions linked to this transaction are also cancelled via the CancelRefundedTransactions listener.
refundedThe 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

# 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"
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);
    }
}
use Siren\Transactions\Core\Facades\Transactions;

$transaction = Transactions::getById(42);

Siren is built on an event-driven architecture. 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.

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;
        }
    }
}
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.

use Siren\Transactions\Core\Datastores\TransactionDetail\Interfaces\TransactionDetailDatastore;

class MyService
{
    protected TransactionDetailDatastore $details;

    public function __construct(TransactionDetailDatastore $details)
    {
        $this->details = $details;
    }
}
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.

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;
    }
}
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.

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.

use Siren\Transactions\Core\Datastores\TransactionDetailAttribute\Interfaces\TransactionDetailAttributeDatastore;

class MyService
{
    protected TransactionDetailAttributeDatastore $attributes;

    public function __construct(TransactionDetailAttributeDatastore $attributes)
    {
        $this->attributes = $attributes;
    }
}
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.

// 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
);
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.

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
}
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.

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.

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 (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 (downstream, via attribution). Manual attribution through the credit-collaborator endpoint creates an engagement for the collaborator, which in turn produces the conversion.
  • Obligation (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 reference.