production writes → app.brandsgateway.com Examples on this page target the live marketplace.
BG Vendor Integration
Menu
Operating it

Examples

Working clients you can lift into your own codebase. Each one covers the same four concerns: authenticate, pace, upsert, reconcile.

Start from these, not from the schema
These already handle the things that are easy to get wrong — the 201 vs 200 distinction, pacing that absorbs response-time variance, and taking a product you no longer carry down to zero stock rather than leaving phantom stock in the shop.

Node.js — a complete sync

The full loop: open a run, fingerprint and skip unchanged products, upsert what changed, zero what departed, close the run. Your only job is transform() — the mapping from your catalog to the product contract.

sync.mjs
const PORTAL = process.env.BG_PORTAL_URL;

const headers = {
  Authorization:
    "Basic " +
    Buffer.from(
      `${process.env.BG_USER}:${process.env.BG_APP_PASSWORD}`
    ).toString("base64"),
  "X-Vendor-Id": process.env.BG_VENDOR_ID,
  "Content-Type": "application/json",
};

/** Add or update one product. Send your own words; we map them. */
async function send(source) {
  const res = await fetch(
    `${PORTAL}/api/v1/products/${encodeURIComponent(source.id)}`,
    {
      method: "PUT",
      headers,
      body: JSON.stringify({
        values: {
          brand:    source.brand,
          gender:   source.gender,
          color:    source.color,
          material: source.composition,
          country:  source.madeIn,
          category: source.categoryPath,
        },
        product: {
          type: source.sizes?.length ? "variable" : "simple",
          name: source.title,
          sku:  source.sku,
          images: source.images.map((src) => ({ src })),
          short_description: source.description,
          ...(source.sizes?.length
            ? { variations: source.sizes.map(toVariation) }
            : {
                regular_price:    money(source.retail),
                sale_price:       money(source.price),
                stock_quantity:   source.stock,
                manage_stock:     true,
                global_unique_id: source.ean,
              }),
          meta_data: [
            { key: "_external_id", value: String(source.id) },
            { key: "_vendor_cost", value: money(source.cost) },
            { key: "_vendor_sku",  value: source.sku },
          ],
        },
      }),
    }
  );

  const body = await res.json();

  if (res.status === 201) return { outcome: "created" };
  if (res.ok)             return { outcome: "updated" };

  // 422 means nothing reached the shop. Either a value did not map, or the
  // product did not pass validation — the body says which.
  return {
    outcome: "rejected",
    unresolved: body.unresolved,
    errors: body.errors,
    code: body.code,
    message: body.message,
  };
}

const money = (n) => Number(n).toFixed(2);

/** Two writes per second, absorbing response-time variance. */
async function paced(tasks, perSecond = 2) {
  const gap = 1000 / perSecond;
  let next = Date.now();
  const out = [];
  for (const task of tasks) {
    const wait = next - Date.now();
    if (wait > 0) await new Promise((r) => setTimeout(r, wait));
    next = Math.max(next + gap, Date.now());
    out.push(await task());
  }
  return out;
}

export async function sync(catalog, previousIds = []) {
  const results = await paced(catalog.map((source) => () => send(source)));

  // Anything you sold last run and no longer carry goes to zero stock. This
  // is the half integrations forget, and the reason phantom stock gets sold.
  const live = new Set(catalog.map((s) => String(s.id)));
  const gone = previousIds.filter((id) => !live.has(String(id)));

  await paced(
    gone.map((id) => () =>
      fetch(`${PORTAL}/api/v1/products/${encodeURIComponent(id)}`, {
        method: "PUT",
        headers,
        body: JSON.stringify({
          product: { type: "simple", manage_stock: true, stock_quantity: 0 },
        }),
      }),
    ),
  );

  return results;
}

PHP — the client

BrandsGatewayClient.php
<?php

final class BrandsGatewayClient
{
    private array $headers;

    public function __construct(
        private string $portal,
        string $vendorId,
        string $user,
        string $appPassword,
    ) {
        $this->headers = [
            'Content-Type: application/json',
            'X-Vendor-Id: ' . $vendorId,
            'Authorization: Basic ' . base64_encode("{$user}:{$appPassword}"),
        ];
    }

    /** @return array{outcome:string, body:array} */
    public function send(string $externalId, array $values, array $product): array
    {
        $url = sprintf(
            '%s/api/v1/products/%s',
            rtrim($this->portal, '/'),
            rawurlencode($externalId),
        );

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => 'PUT',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 90,
            CURLOPT_HTTPHEADER     => $this->headers,
            CURLOPT_POSTFIELDS     => json_encode(
                ['values' => $values, 'product' => $product],
                JSON_UNESCAPED_SLASHES,
            ),
        ]);

        $raw    = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        $body = json_decode((string) $raw, true) ?: [];

        return [
            'outcome' => match (true) {
                $status === 201 => 'created',
                $status < 300   => 'updated',
                default         => 'rejected',
            },
            'body' => $body,
        ];
    }

    /** A product you no longer carry: zero its stock, never delete it. */
    public function zero(string $externalId): array
    {
        return $this->send($externalId, [], [
            'type'           => 'simple',
            'manage_stock'   => true,
            'stock_quantity' => 0,
        ]);
    }
}

Python — the client

brandsgateway.py
import base64, time
import httpx


class BrandsGateway:
    def __init__(self, portal: str, vendor_id: str, user: str, app_password: str):
        token = base64.b64encode(f"{user}:{app_password}".encode()).decode()
        self.portal = portal.rstrip("/")
        self.client = httpx.Client(
            headers={
                "Authorization": f"Basic {token}",
                "X-Vendor-Id": vendor_id,
            },
            timeout=90.0,
        )
        self._next_write = 0.0

    def _pace(self, per_second: float = 2.0) -> None:
        wait = self._next_write - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        self._next_write = max(self._next_write + 1 / per_second, time.monotonic())

    def send(self, external_id: str, values: dict, product: dict) -> dict:
        """Add or update. `values` is your own vocabulary; we map it."""
        self._pace()
        res = self.client.put(
            f"{self.portal}/api/v1/products/{external_id}",
            json={"values": values, "product": product},
        )
        body = res.json()

        if res.status_code == 201:
            return {"outcome": "created", **body}
        if res.is_success:
            return {"outcome": "updated", **body}

        # 422: nothing reached the shop. `unresolved` means a value did not
        # map; `errors` means the product failed validation.
        return {"outcome": "rejected", **body}

    def zero(self, external_id: str) -> dict:
        """A product you no longer carry: zero its stock, never delete it."""
        return self.send(
            external_id,
            {},
            {"type": "simple", "manage_stock": True, "stock_quantity": 0},
        )

Validating in CI

The most useful place for the validator is your pipeline, not your browser. Fail the build when a feed change breaks the contract, and you will never ship a run that rejects thousands of products.

validate.sh
# Validate a whole directory of payloads in CI, before anything
# reaches BrandsGateway. Exit non-zero on the first blocking error.

for f in payloads/*.json; do
  result=$(curl -s -X POST https://vendors.brandsgateway.com/api/v1/validate \
    -H 'Content-Type: application/json' --data @"$f")

  if [ "$(echo "$result" | jq -r .valid)" != "true" ]; then
    echo "FAIL $f"
    echo "$result" | jq -r '.errors[] | "  \(.path): \(.message)"'
    exit 1
  fi
done