Skip to content

Example: catalog synchronisation

A complete scenario for getting a catalog from your own system into Metty: a nightly full sync, incremental changes during the day, and verification of the result.

Nightly full sync

A full sync uploads the whole catalog and removes everything that is no longer part of it. It is the only operation that can delete products without listing them.

php
use Metty\Client\Catalog\CatalogProduct;
use Metty\Client\Exception\SyncIncompleteException;
use Metty\Client\MettyClient;

$client = MettyClient::create(secretKey: getenv('METTY_SECRET_KEY'));

function catalog(PDO $database): Generator
{
    $statement = $database->query('SELECT * FROM product WHERE active = 1');

    while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
        yield CatalogProduct::create(
            id: $row['sku'],
            name: $row['name'],
            url: 'https://shop.example/product/' . $row['slug'],
            price: (float) $row['price'],
            listPrice: $row['price_before'] === null ? null : (float) $row['price_before'],
            currency: 'EUR',
            inStock: $row['stock'] > 0,
            brand: $row['brand'],
            category: $row['category_path'],
            description: $row['description'],
            image: $row['image_url'],
            params: json_decode($row['attributes'], true) ?? [],
        );
    }
}

try {
    $outcome = $client->catalog()->synchronize(catalog($database));

    printf("Sync %s: %d kept, %d removed\n",
        $outcome['sync_id'],
        $outcome['commit']['kept'],
        $outcome['commit']['removed'],
    );
} catch (SyncIncompleteException $exception) {
    foreach ($exception->result->failures() as $failure) {
        fprintf(STDERR, "%s: %s\n", $failure->id, $failure->error);
    }

    exit(1);
}
bash
#!/usr/bin/env bash
set -euo pipefail

AUTH="Authorization: Bearer $METTY_SECRET_KEY"
BASE="https://catalog.api.metty.eu"

SYNC=$(curl -sf -X POST "$BASE/catalog/syncs" -H "$AUTH" | jq -r .sync_id)

for batch in batches/*.json; do
  curl -sf -X PUT "$BASE/catalog/products?sync=$SYNC" \
    -H "$AUTH" -H 'Content-Type: application/json' \
    --data-binary "@$batch" \
  | jq -e '[.results[] | select(.status != "ok")] | length == 0' > /dev/null \
  || { echo "Batch $batch contains errors, sync $SYNC will not be committed"; exit 1; }
done

curl -sf -X POST "$BASE/catalog/syncs/$SYNC/commit" -H "$AUTH" | jq

The order matters: commit only after every batch has been verified. If half the catalog failed to upload and the sync were committed anyway, the products that did not make it would disappear from the e-shop. The PHP client applies this safeguard itself and throws a SyncIncompleteException with the syncId still open, so it can be finished later.

If you are deliberately shrinking the catalog by more than half, the server rejects the commit with 409 generation_incomplete. The explicit override is force:

php
$client->catalog()->synchronize($products, force: true);

Incremental changes during the day

Do not push price and availability changes through a full sync — a PATCH with the fields that actually changed is enough.

php
use Metty\Client\Catalog\CatalogProduct;

$result = $client->catalog()->patch([
    new CatalogProduct('sku-1', ['price' => 99.9, 'list_price' => 129.9]),
    new CatalogProduct('sku-2', ['in_stock' => false]),
    new CatalogProduct('sku-3', ['list_price' => null]),
]);

foreach ($result->failures() as $failure) {
    error_log(sprintf('%s: %s — %s', $failure->id, $failure->error, $failure->message));
}
bash
curl -sf -X PATCH https://catalog.api.metty.eu/catalog/products \
  -H "Authorization: Bearer $METTY_SECRET_KEY" \
  -H 'Content-Type: application/json' \
  -d '[
    { "id": "sku-1", "price": 99.9, "list_price": 129.9 },
    { "id": "sku-2", "in_stock": false },
    { "id": "sku-3", "list_price": null }
  ]'

An explicit null clears the field, an omitted field keeps its current value. In the PHP client use the CatalogProduct constructor with a field map for clearing, not the create() factory, which drops null values.

Send new products and products whose whole content is rewritten through replace(), or PUT. Discontinued products are removed in a single batch:

php
$client->catalog()->delete(['sku-9', 'sku-10']);

Verifying the state

The export returns exactly what Metty holds, in the same shape PUT accepts:

php
$byId = [];
foreach ($client->catalog()->export() as $product) {
    $byId[$product['id']] = $product;
}

printf("Metty holds %d products\n", count($byId));

Comparing the export against your own database is the fastest way to spot products that never reached Metty.

intervaloperation
once a day, off-peakfull sync of the whole catalog
every few minutesPATCH of prices and availability
when a product is publishedPUT of a single product
when a product is discontinuedDELETE
weeklycomparison of the export against your own database

The limit for /catalog/* is 120 requests per minute, with one extra request counted for every 256 KB of body. A nightly sync of a catalog with tens of thousands of products fits comfortably; the details are in Errors and limits.

Metty documentation