Skip to main content

POS integration guide

You run a POS platform. Each of your retailer customers has their own store branding, product catalog, and image folder — today filled by hand from your central /images/{store_id}/ share whenever a new product ships. This guide walks through replacing that manual sync with the Retail Digitals Image API.

The pattern in one paragraph​

Issue one API client per retailer (not one for your whole platform). Each store's POS gets its own client_id + api_key. Product lookups happen at scan time from the POS itself. Overnight, a nightly sync fills a small per-store image cache from the API so the POS doesn't have to hit the network on every scan. The 15-minute signed URL TTL means cached URLs are useless if they leak — but the cached image bytes are already watermarked and traceable back to that specific store.

Why per-store clients, not one platform client​

Rate limits are per-client. If store #47 has a bad night and hammers the API, only their POS is throttled — every other store keeps running.

Attribution is per-client. Every delivered image has a per-client_id invisible watermark. If leaked images show up on a scraper site, we tell you which store's credentials leaked — not just "somewhere in your platform."

Scope is per-client. You can restrict store A to see only brands they carry, without setting up cross-store filtering in your own code.

Revocation is per-client. Store leaves your platform → disable that client → their POS loses access instantly. Your other stores are untouched.

Billing is per-client. Bill each store for their own usage. /usage gives you a clean per-client cost breakdown for month-end invoicing.

Setup steps​

1. Register your platform account​

Create one master account at images.retaildigitals.com/register under your company. This is the billing account — all per-store clients bill up to this account's credit balance.

2. Issue store-scoped API clients​

For each retailer store, in your account panel:

  1. Go to API Keys → New client
  2. Name: pos-{store_id} (e.g. pos-brooklyn-supermarket-042)
  3. Scope: full-catalog (or a brand filter if the retailer only carries certain lines)
  4. IP allowlist: the store's static IP if they have one — leave open otherwise
  5. Rate limits: 60 rpm / 10,000 rpd (default Standard). Bump for very high-volume stores.
  6. Copy the api_key and store it in your provisioning system — we won't show it again

Automate this — hitting New client by hand 500 times isn't scalable. Contact api@retaildigitals.com for the management API endpoint that lets you provision clients programmatically.

3. Push credentials to the store's POS​

However you deploy POS config today (Chef, Ansible, MDM, USB-key install), add two entries per store:

RD_CLIENT_ID=rd_client_01h8y3g7z8mnpqrsw
RD_API_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Never put the platform-level api_key on any POS. Only per-store credentials.

4. Implement scan-time lookup​

The POS's scan handler:

function onBarcodeScan(string $barcode): array {
// Cache metadata locally — it rarely changes
$cached = $cache->get("product:{$barcode}");
if ($cached && $cached['fetched_at'] > time() - 86400) {
return $cached;
}

// Fetch with retry. Both parameters matter for cost — include= picks
// metadata scopes (each charged per_scope_rate), variants= picks image URLs
// (each charged per_image_rate). If your POS only displays the front image,
// pass BOTH include=[product_meta, images] AND variants=[front] to skip
// image_meta and the other 3 variants' URLs. See /developers/pricing for
// the full cost formula and current admin-set rates.
$product = $rdClient->getProduct($barcode, include: ['product_meta', 'images'], variants: ['front']);

if (!$product) {
// Unknown barcode — fall back to store's own catalog entry
return $localCatalog->lookup($barcode);
}

// Cache the metadata; do NOT cache the signed image URL (15-min TTL)
$cache->set("product:{$barcode}", [
'metadata' => $product['product_meta'],
'fetched_at' => time(),
], ttl: 86400);

// Download the image immediately if we don't already have it
if (!file_exists("/var/lib/pos/images/{$barcode}.jpg")) {
file_put_contents(
"/var/lib/pos/images/{$barcode}.jpg",
file_get_contents($product['images']['front'])
);
}

return $product;
}

Note the two caches:

  • Metadata cache — 24h TTL, /products/{barcode} metadata rarely changes
  • Image cache — permanent on-disk, images never change after upload

For stores with stable catalogs, run a nightly job that only pulls what's changed since last sync — much cheaper than a full refresh. Use updated_since on GET /products to enumerate the delta, then fetch full metadata + images only for those barcodes. Retry-safe with Idempotency-Key.

#!/bin/bash
# Nightly at 03:00 store-local time
export RD_CLIENT_ID=$(cat /etc/pos/client_id)
export RD_API_KEY=$(cat /etc/pos/api_key)

# Where we saved the timestamp of the previous successful sync
LAST_SYNC=$(cat /var/lib/pos/last_sync 2>/dev/null || echo "1970-01-01")
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# 1. Enumerate the delta: which barcodes changed since last sync?
# /products list is free — no per-barcode cost for this scan.
BARCODES=$(curl -sS -H "Authorization: Bearer $(get_cached_token)" \
"https://images.retaildigitals.com/api/v1/products?updated_since=$LAST_SYNC&per_page=500" \
| jq -r '.data[].barcode')

# 2. Prefilter: which of those barcodes actually HAVE metadata worth fetching?
# Coverage is partial, so without this step most include= calls are charged
# for an empty payload. Availability checks never return metadata values.
echo "$BARCODES" | jq -R -s -c 'split("\n") | map(select(length>0))' > /tmp/delta.json
curl -sS -X POST -H "Authorization: Bearer $(get_cached_token)" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: meta-avail-$(date -u +%Y-%m-%d)" \
-d "{\"barcodes\": $(cat /tmp/delta.json)}" \
"https://images.retaildigitals.com/api/v1/metadata/bulk-check" \
> /tmp/availability.json

# 3. Fetch product_meta + front image only for barcodes that have something
for barcode in $(jq -r '.results | to_entries[] | select(.value.exists and .value.metadata.info) | .key' /tmp/availability.json); do
curl -sS -H "Authorization: Bearer $(get_cached_token)" \
"https://images.retaildigitals.com/api/v1/products/$barcode?include=product_meta,images&variants=front" \
> "/var/lib/pos/metadata/$barcode.json"
done

# 4. Optionally batch-verify which of your store-catalog barcodes still exist in our catalog.
# Idempotency-Key protects against duplicate charges if this job is retried.
psql -c "SELECT barcode FROM store_catalog" | jq -R -s -c 'split("\n") | map(select(length>0))' > /tmp/barcodes.json
curl -sS -X POST \
-H "Authorization: Bearer $(get_cached_token)" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: nightly-sync-$(date -u +%Y-%m-%d)" \
-d "{\"barcodes\": $(cat /tmp/barcodes.json)}" \
"https://images.retaildigitals.com/api/v1/products/bulk-check" \
> /var/lib/pos/existence-check.json

# 5. Record this sync's timestamp for the next run
echo "$NOW" > /var/lib/pos/last_sync

Two cost-saving levers to combine:

  1. variants= on /products/{barcode} — request only the image variants your POS actually renders. variants=front charges 1× per_image_rate instead of 4× for a 4-variant product.
  2. POST /products/bulk-check as a pre-filter — 500 barcodes per call at products.bulk_check.per_barcode each — skips barcodes not in our catalog before spending a full-GET per known miss.

Combining both, a nightly refresh of 2,000 SKUs that only need the front image typically costs under $30/month at Standard tier pricing.

Cost budgeting for POS​

Rough monthly cost for a mid-sized store. Numbers assume variants=front (POS displays front only); scale by ×N if the POS renders N variants.

ScenarioCredits/monthCost/month
2,000 SKU nightly refresh, default (all 4 variants)90,000$770
2,000 SKU nightly refresh, variants=front only30,000$270
2,000 SKU weekly refresh + daily bulk-check, variants=front8,600$77
500 scans/day, all cache hits after day 15,000$45
Same as above with Enterprise 40% volume discount5,000$30

For POS platforms with 100+ stores, ask about Enterprise fixed-pricing bundles.

Handling store offboarding​

When a retailer leaves your platform:

  1. Log into your account panel → API Keys → find pos-{store_id} → Revoke
  2. Their POS immediately gets 403 client_disabled on next auth attempt
  3. Any cached signed image URLs expire naturally within 15 minutes
  4. Cached image bytes on the POS remain accessible offline — but they still carry that store's per-client watermark, so if a copy shows up publicly you can identify it

Support for POS partners​

We treat POS platforms as tier-1 integration partners. Sign up at api@retaildigitals.com to get:

  • Programmatic client provisioning API
  • Aggregated cross-client dashboards
  • Volume discount tiers based on active-store count
  • Shared Slack channel for engineering questions
  • Advance notice on all API changes (before public changelog)