Automation

Keep working the way you do now.

Your PIM, ERP, DAM or a shared folder already has the data and the photos. Pick the first recipe that fits: they work with every Open Brand Exchange portal.

Terminal
$ obx push ./export
Publishing to brands.kestrel.example (Kestrel, Kestrel Trail)
images: 27 checked, 3 uploaded (4.2 MB) in 3s
Done: rows: 40, images: 27 → new: 2 · updated: 3 · unchanged: 35
publish.ymlGitHub Action
# .github/workflows/publish.yml
on:
  push:
    paths: ["export/**"]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: openbrandexchange/obx-go/push-action@v0
        with:
          url: ${{ secrets.OBX_URL }}
          token: ${{ secrets.OBX_TOKEN }}
          folder: export

Which recipe fits you?

You have…UseCode needed
a CSV or zip on a web address, SFTP server or S3 bucketScheduled importnone
a folder where exports landobx watchnone
a server or PC that runs nightly jobscron or Task Schedulerone line
your exports in a Git repositoryGitHub Actionone YAML file
n8n, Make or ZapierHTTP stepsnone (visual)
a developer who connects your PIM or ERPThe API directlya little

One format for everything

Every way of publishing reads the same thing: a products.csv with one row per size, and photos named {style}_{color}_{position}.jpg. Once you have that, every recipe below works.

products.csv (shortened)
brand,style_number,name:en,name:nl,item_type,season,materials,color_code,color_name:en,size,gtin
Kestrel,KO-1001,Merino Crew Sweater,Merino trui met ronde hals,sweater,AW26,main: 100% merino_wool,NVY,Navy,S,2000100101018
Kestrel,KO-1001,Merino Crew Sweater,Merino trui met ronde hals,sweater,AW26,main: 100% merino_wool,NVY,Navy,M,2000100101025
Photo file names
images/
  KO-1001_NVY_1.jpg     ← style KO-1001, colour NVY, position 1 (front)
  KO-1001_NVY_2.jpg
  KO-1001_NVY_3.jpg

The full import format

What you need

In your admin, go to Automatic upload → Access keys and create a new key. You then have two values: the address of your portal (OBX_URL) and the key itself (OBX_TOKEN). The key is shown only once.

export OBX_URL=https://brands.kestrel.example
export OBX_TOKEN=obxi_…   # admin → Automatic upload → Access keys (shown once)

Give every system its own key. Then you can revoke one without breaking the others.

1. Scheduled import (no code)

In your admin: Automatic upload → Scheduled imports → Add scheduled import. Choose the source and how often it runs.

  • Web address (HTTPS): the address of a products.csv, or of a zip with products.csv and photos. Login details are stored encrypted.
  • SFTP: server, user, password or key, and the folder with the export.
  • S3 bucket: AWS S3, Cloudflare R2, MinIO or Backblaze: endpoint, bucket, folder and a read-only key.

Choose "Only add and update" or "Also take missing articles offline". The second also takes offline what disappeared from the file, but stops when more than half of a brand would disappear, so a broken export never empties your catalog. "Test connection" shows which files it would read.

The obx tool

obx is a single program for Windows, macOS and Linux. It reads OBX_URL and OBX_TOKEN from the environment, uploads only new or changed photos, makes the web versions when needed and waits until your portal has processed everything. Run it as often as you like: unchanged articles cost nothing.

obx info                                            # check the token, see limits and brands
obx push --csv products.csv --images ./images       # publish a CSV and a folder (or zip) of images
obx push --package export.zip                       # the same, as one zip
obx push --csv products.csv --images ./images --mirror   # also take offline what is no longer in the file
obx push --csv products.csv --images ./images --dry-run  # only check, change nothing
obx watch ./outbox                                  # keep publishing whatever lands in a folder
obx status                                          # recent jobs and their errors

Exit code 0 = all good, 2 = done but some rows had errors (they are listed), 1 = failed.

Download obx · obx-go

2. A folder that publishes itself: obx watch

Let your PIM, ERP or a colleague save exports into a folder. obx watch publishes whenever something changes and skips photos that stayed the same. Run it as a service, or with Docker:

obx watch /data/outbox
Docker
docker run -d --restart unless-stopped \
  -e OBX_URL -e OBX_TOKEN \
  -v /data/outbox:/outbox \
  ghcr.io/openbrandexchange/obx watch /outbox

3. cron or Windows Task Scheduler

Linux or macOS
# crontab -e: every night at 02:30
30 2 * * * OBX_URL=https://brands.kestrel.example OBX_TOKEN=obxi_… /usr/local/bin/obx push --csv /exports/products.csv --images /exports/images >> /var/log/obx.log 2>&1
Windows (PowerShell, once)
$action = New-ScheduledTaskAction -Execute "C:\Tools\obx.exe" -Argument "push --csv D:\Export\products.csv --images D:\Export\images"
$trigger = New-ScheduledTaskTrigger -Daily -At 2:30am
[Environment]::SetEnvironmentVariable("OBX_URL", "https://brands.kestrel.example", "Machine")
[Environment]::SetEnvironmentVariable("OBX_TOKEN", "obxi_…", "Machine")
Register-ScheduledTask -TaskName "Publish to OBX" -Action $action -Trigger $trigger -User "SYSTEM"

4. GitHub Action

Store OBX_URL and OBX_TOKEN as repository secrets. The same works in GitLab CI, Bitbucket or Azure DevOps: install obx and run obx push.

# .github/workflows/publish.yml  (secrets: OBX_URL, OBX_TOKEN)
name: Publish product content
on:
  push:
    branches: [main]
    paths: ["export/**"]
  schedule:
    - cron: "30 2 * * *"
  workflow_dispatch:
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install obx
        run: |
          curl -fsSL https://github.com/openbrandexchange/obx-go/releases/latest/download/obx_linux_amd64.tar.gz | tar -xz obx
          sudo mv obx /usr/local/bin/
      - name: Publish
        env:
          OBX_URL: ${{ secrets.OBX_URL }}
          OBX_TOKEN: ${{ secrets.OBX_TOKEN }}
        run: obx push --csv export/products.csv --assets-csv export/assets.csv --images export/images

5. n8n, Make or Zapier

Use a plain HTTP Request step with the header Authorization: Bearer followed by your key. Every step is safe to repeat: sending the same data twice changes nothing.

Article (for example when it changes in your PIM)
PUT {OBX_URL}/obx/v1/ingest/products/KO-1001
Authorization: Bearer obxi_…
Content-Type: application/json

{"style_number": "KO-1001",
 "name": {"en": "Merino Crew Sweater", "nl": "Merino trui met ronde hals"},
 "item_type": "sweater", "season": "AW26",
 "materials": [{"part": "main", "fibers": [{"fiber": "merino_wool", "percentage": 100}]}]}
Colour and size, with its EAN
PUT {OBX_URL}/obx/v1/ingest/variants/2000100101025
Authorization: Bearer obxi_…
Content-Type: application/json

{"product_external_id": "KO-1001", "gtin": "2000100101025",
 "color": {"code": "NVY", "name": {"en": "Navy"}}, "size": "M"}
Photo: first the details, then the file itself
PUT {OBX_URL}/obx/v1/ingest/assets/KO-1001_NVY_1
Authorization: Bearer obxi_…
Content-Type: application/json

{"product_external_id": "KO-1001", "color_code": "NVY",
 "role": "packshot", "view": "front", "position": 1}

PUT {OBX_URL}/obx/v1/ingest/assets/KO-1001_NVY_1/files/original
Authorization: Bearer obxi_…
Content-Type: image/jpeg

<the image file as binary body>

The Portal for Cloudflare does not make web versions of photos itself. Publish with obx or with "Imports" in your admin there: they make them for you. Photos that arrive another way, such as from a scheduled import, are shown in their original size until then.

Taking something offline
PATCH {OBX_URL}/obx/v1/ingest/products/KO-1001
Authorization: Bearer obxi_…
Content-Type: application/merge-patch+json

{"status": "unpublished"}

Want a signal when something happens? In your admin, add a notification (webhook) for a finished import or a new access request, and let it start an n8n, Make or Zapier trigger.

6. The API directly (for developers)

An integration remembers what it sent last time and sends only the changes, in batches. Or send the CSV as it is and let the portal read it.

Skip photos the portal already has, then send one batch
URL="$OBX_URL/obx/v1/ingest"; AUTH="Authorization: Bearer $OBX_TOKEN"

# 1. Upload only the images the node does not have yet (addressed by sha256).
for f in images/*.jpg; do
  sha=$(shasum -a 256 "$f" | cut -d' ' -f1)
  curl -sf -I -H "$AUTH" "$URL/blobs/$sha" >/dev/null || \
    curl -sf -X PUT -H "$AUTH" -H 'Content-Type: image/jpeg' --data-binary @"$f" "$URL/blobs/$sha"
done

# 2. One batch of up to 1000 operations (NDJSON), processed in order.
cat > batch.ndjson <<'EOF'
{"op":"upsert","kind":"product","external_id":"KO-1001","data":{"style_number":"KO-1001","name":{"en":"Merino Crew Sweater"}}}
{"op":"upsert","kind":"variant","external_id":"2000100101025","data":{"product_external_id":"KO-1001","gtin":"2000100101025","color":{"code":"NVY"},"size":"M"}}
{"op":"unpublish","kind":"product","external_id":"KO-0999"}
EOF
curl -s -X POST -H "$AUTH" -H 'Content-Type: application/x-ndjson' \
  -H "Idempotency-Key: nightly-$(date +%F)" --data-binary @batch.ndjson "$URL/batch"
A CSV and a zip of photos in one go
curl -s -X POST -H "Authorization: Bearer $OBX_TOKEN" \
  -F [email protected] -F [email protected] \
  -F 'options={"mode":"upsert"};type=application/json' \
  "$OBX_URL/obx/v1/ingest/imports"          # returns a job; poll $OBX_URL/obx/v1/ingest/jobs/<id>

Every detail is in the specification:Ingest API ·automation recipes ·Go SDK ·Developers