Productsup

Export delta connector

Send only the products that changed since the last run.

An export-delta connector works like an export connector, but instead of sending all products to the channel every run, it determines which products to send based on what changed since the last run. The platform tracks product data statuses between runs and provides four separate input streams — new, modified, unchanged, and deleted — so your connector can perform the appropriate action for each product (create, update, skip, or delete).

Use export-delta when the third-party channel supports incremental updates — it's more efficient and reduces API calls.

Data flow

Productsup platform → Container API (4 delta streams) → Your connector → Third-party channel

                                                        Container API (feedback) → Productsup platform

Instead of a single input file, the Container API provides four streams based on what changed:

StreamDescription
NewProducts added to the site since the last run
ModifiedProducts that changed since the last run
UnchangedProducts with no changes since the last run
DeletedProducts removed since the last run

How it works at runtime

  1. The platform starts your Docker container and passes export settings as environment variables
  2. Your code reads products from one or more of the four delta streams
  3. For each product, you perform the appropriate action on the third-party channel (create, update, skip, or delete)
  4. You write feedback for each product (success or failure)
  5. You send progress notifications to the Productsup notification panel
  6. Exit with code 0 (success) or non-zero (failure)
  7. The platform imports the feedback file as a data source on the next site run

Reading delta input

Each stream has its own methods for iterating and counting.

Iterate by stream

// New products — create them on the channel
foreach ($this->containerApi->yieldFromNewFile() as $product) {
    $this->createOnChannel($product);
}

// Modified products — update them on the channel
foreach ($this->containerApi->yieldFromModifiedFile() as $product) {
    $this->updateOnChannel($product);
}

// Unchanged products — usually skipped
foreach ($this->containerApi->yieldFromUnchangedFile() as $product) {
    // No action needed in most cases
}

// Deleted products — remove them from the channel
foreach ($this->containerApi->yieldFromDeletedFile() as $product) {
    $this->deleteFromChannel($product);
}

Iterate in batches

Each stream also supports batch iteration:

foreach ($this->containerApi->yieldBatchFromNewFile(100) as $batch) {
    $this->createBatchOnChannel($batch);
}

foreach ($this->containerApi->yieldBatchFromModifiedFile(100) as $batch) {
    $this->updateBatchOnChannel($batch);
}

Count items per stream

$newCount = $this->containerApi->countItemsFromNewFile();
$modifiedCount = $this->containerApi->countItemsFromModifiedFile();
$unchangedCount = $this->containerApi->countItemsFromUnchangedFile();
$deletedCount = $this->containerApi->countItemsFromDeletedFile();

Writing feedback

Same as export connectors — write feedback for each product to report success or failure. The platform imports the feedback file as an additional data source on the next site run.

$this->containerApi->appendToFeedbackFile([
    'id' => $product['id'],
    'status' => 'success',
]);

Each export destination can have at most one feedback file. See Feedback as an additional data source for how connection columns work.

Logging and notifications

Use log methods for operational messages visible in the Dev Portal run logs. Use notifications for end-user-facing messages that appear in the Productsup notification panel. End-users see notifications in a collapsible panel at the bottom of their site view:

Notification panel in the Productsup platform — end-users see connector messages here

// Logging — visible in Dev Portal run logs
$this->containerApi->info("Processing 50 new, 20 modified, 5 deleted products.");
$this->containerApi->warning("Product {$id} not found on channel, skipping delete.");
$this->containerApi->error("Channel API returned 500 for product {$id}.");

// Notifications — visible to end-users in the Productsup notification panel
$this->containerApi->sendNotification('success', 'Delta export completed: 75 products processed.');
$this->containerApi->sendNotification('warning', 'Delta export completed with 3 errors.');

Available log levels: debug, info, notice, warning, error, critical, alert, emergency.

Available notification levels: info, notice, warning, error, success.

Example service

A minimal export-delta service that processes each delta stream, writes feedback, and reports progress:

<?php

namespace App\ExportDelta\Service;

use Productsup\CDE\ContainerApi\ContainerApiInterface;

readonly class ExportDeltaService
{
    public function __construct(
        private ContainerApiInterface $containerApi,
    ) {}

    public function run(): void
    {
        $newCount = $this->containerApi->countItemsFromNewFile();
        $modifiedCount = $this->containerApi->countItemsFromModifiedFile();
        $deletedCount = $this->containerApi->countItemsFromDeletedFile();

        $this->containerApi->info("Delta export: {$newCount} new, {$modifiedCount} modified, {$deletedCount} deleted.");

        $errorCount = 0;

        $errorCount += $this->processStream(
            $this->containerApi->yieldFromNewFile(),
            fn(array $product) => $this->createOnChannel($product),
        );

        $errorCount += $this->processStream(
            $this->containerApi->yieldFromModifiedFile(),
            fn(array $product) => $this->updateOnChannel($product),
        );

        $errorCount += $this->processStream(
            $this->containerApi->yieldFromDeletedFile(),
            fn(array $product) => $this->deleteFromChannel($product),
        );

        $totalProcessed = $newCount + $modifiedCount + $deletedCount;

        if ($errorCount > 0) {
            $this->containerApi->sendNotification('warning', "Delta export completed with {$errorCount} errors out of {$totalProcessed} products.");

            return;
        }

        $this->containerApi->sendNotification('success', "Delta export completed: {$totalProcessed} products processed.");
    }

    private function processStream(\Traversable $stream, \Closure $action): int
    {
        $errorCount = 0;

        foreach ($stream as $product) {
            try {
                $action($product);

                $this->containerApi->appendToFeedbackFile([
                    'id' => $product['id'],
                    'status' => 'success',
                ]);
            } catch (\Throwable $e) {
                $errorCount++;

                $this->containerApi->warning("Failed to process product {$product['id']}: {$e->getMessage()}");

                $this->containerApi->appendToFeedbackFile([
                    'id' => $product['id'],
                    'status' => 'error',
                    'message' => $e->getMessage(),
                ]);
            }
        }

        return $errorCount;
    }

    private function createOnChannel(array $product): void { /* Your create API call */ }
    private function updateOnChannel(array $product): void { /* Your update API call */ }
    private function deleteFromChannel(array $product): void { /* Your delete API call */ }
}

Export vs. export delta

ExportExport delta
InputAll products every runOnly products that changed
Streams1 (input)4 (new, modified, unchanged, deleted)
Best forChannels that expect a full feedChannels that support incremental updates
EfficiencyProcesses everything every timeOnly processes changes

Your connector type must match the input it requests. An export connector requesting new or modified input, or an export-delta connector requesting full input, will cause an error.

Configuration

Export-delta connectors share the same type-specific configuration as export connectors: channels, feedback file, and category-specific attributes.

See Type-specific config for details.

Next steps

How is this guide?

On this page