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. For each changed barcode, fetch product_meta + front image only
for barcode in $BARCODES; 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

# 3. 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

# 4. 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)