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:
- Go to API Keys → New client
- Name:
pos-{store_id}(e.g.pos-brooklyn-supermarket-042) - Scope:
full-catalog(or a brand filter if the retailer only carries certain lines) - IP allowlist: the store's static IP if they have one — leave open otherwise
- Rate limits: 60 rpm / 10,000 rpd (default Standard). Bump for very high-volume stores.
- Copy the
api_keyand 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
5. Nightly incremental sync (recommended)
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:
variants=on/products/{barcode}— request only the image variants your POS actually renders.variants=frontcharges 1×per_image_rateinstead of 4× for a 4-variant product.POST /products/bulk-checkas a pre-filter — 500 barcodes per call atproducts.bulk_check.per_barcodeeach — skips barcodes not in our catalog before spending a full-GETper 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.
| Scenario | Credits/month | Cost/month |
|---|---|---|
| 2,000 SKU nightly refresh, default (all 4 variants) | 90,000 | $770 |
2,000 SKU nightly refresh, variants=front only | 30,000 | $270 |
2,000 SKU weekly refresh + daily bulk-check, variants=front | 8,600 | $77 |
| 500 scans/day, all cache hits after day 1 | 5,000 | $45 |
| Same as above with Enterprise 40% volume discount | 5,000 | $30 |
For POS platforms with 100+ stores, ask about Enterprise fixed-pricing bundles.
Handling store offboarding
When a retailer leaves your platform:
- Log into your account panel → API Keys → find
pos-{store_id}→ Revoke - Their POS immediately gets
403 client_disabledon next auth attempt - Any cached signed image URLs expire naturally within 15 minutes
- 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)