Getting started
Everything you need to make your first authenticated API call — in about 5 minutes.
If you already have an account, jump straight to Step 2 — Issue an API key.
Prerequisites
You'll need:
- A Retail Digitals account (free to create — sign up →)
- A tool to send HTTP requests. Any of these work:
- Terminal with
curl(built into macOS / Linux, available on Windows 10+) - Postman or Insomnia (both free)
- Your language runtime — Node.js, PHP, Python, whatever you're building in
- Terminal with
- A few credits in your balance. New accounts start with 10 free credits — enough to make a few requests to try things out (the exact number depends on how you shape each call — see Cost model). Top up when you're ready to build production traffic.
Step 1 — Create an account
Head to images.retaildigitals.com/register. It's the same account you'd use for the web UI order flow — the API just reuses your login. Your credit balance is shared between UI orders and API usage.
The 10 free credits arrive automatically. Check your balance any time at images.retaildigitals.com/billing.
Step 2 — Issue an API key
Once logged in, navigate to Account → API Keys (or go direct to images.retaildigitals.com/account/api-keys).
Click "Create new API client", give it a name that'll help you identify it later (e.g. "Website production", "POS staging"), and hit Create.
You'll see a screen like this:
┌────────────────────────────────────────────────────────────────┐
│ ⚠ This is the only time you'll see the API key. │
│ Copy it now and store it somewhere safe. │
│ │
│ Client ID: rd_client_01h8y3g7z8mnpqrsw │
│ [📋] │
│ │
│ API Key: ••••••••••••••••••••••••••••••••••••• [👁] │
│ [📋] │
│ │
│ [ Done ] │
└────────────────────────────────────────────────────────────────┘
client_idis safe to log, embed in URLs, put in your version control. It's your public identifier.api_keyis a secret. Click the 👁 to reveal, then 📋 to copy. You cannot retrieve it later — if you lose it, come back and rotate it (which invalidates the old one).
Save both somewhere secure. Environment variables are ideal:
- .env file
- Shell (temporary)
- Windows PowerShell
# Add these to your .env (never commit to git!)
RD_CLIENT_ID=rd_client_01h8y3g7z8mnpqrsw
RD_API_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export RD_CLIENT_ID="rd_client_01h8y3g7z8mnpqrsw"
export RD_API_KEY="sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$env:RD_CLIENT_ID = "rd_client_01h8y3g7z8mnpqrsw"
$env:RD_API_KEY = "sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Never commit your API key. Never paste it into browser DevTools or a public GitHub issue. If a key leaks, rotate it immediately at Account → API Keys → Rotate — the old one is dead within 5 seconds.
Step 3 — Exchange credentials for an access token
The API uses a two-step auth: your long-lived client_id + api_key is used once to obtain a short-lived access token (1 hour) plus a refresh token (90 days). Every subsequent API call uses only the access token.
Send your first request:
- curl (macOS / Linux / WSL / Git Bash)
- Windows PowerShell
- JavaScript (Node)
- PHP
- Python
curl -X POST https://images.retaildigitals.com/api/v1/auth/token \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"$RD_CLIENT_ID\",\"api_key\":\"$RD_API_KEY\"}"
Windows PowerShell uses $env:VAR (not $VAR) — and cmd.exe has neither.
Substitute the raw values inline, or use PowerShell:
$body = @{ client_id = $env:RD_CLIENT_ID; api_key = $env:RD_API_KEY } | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri 'https://images.retaildigitals.com/api/v1/auth/token' `
-ContentType 'application/json' -Body $body
const res = await fetch('https://images.retaildigitals.com/api/v1/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.RD_CLIENT_ID,
api_key: process.env.RD_API_KEY,
}),
});
const auth = await res.json();
console.log(auth);
<?php
$ch = curl_init('https://images.retaildigitals.com/api/v1/auth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'client_id' => getenv('RD_CLIENT_ID'),
'api_key' => getenv('RD_API_KEY'),
]),
CURLOPT_RETURNTRANSFER => true,
]);
$auth = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($auth);
import os, requests
auth = requests.post(
"https://images.retaildigitals.com/api/v1/auth/token",
json={
"client_id": os.environ["RD_CLIENT_ID"],
"api_key": os.environ["RD_API_KEY"],
},
).json()
print(auth)
You'll get back:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "rt_01h8y3g7z8mnpqrsw...",
"token_type": "Bearer",
"expires_in": 3600
}
access_token— attach asAuthorization: Bearer <token>on every subsequent call. Lives for 3600 seconds (1 hour).refresh_token— save it. When your access token expires, exchange the refresh token for a new access token without needing your original credentials. See Authentication → Refresh flow for details.
Save the access token in your environment for the rest of this guide. Paste the
value from the access_token field of the response above — not the whole JSON blob
(that's the #1 first-request mistake):
- Bash / zsh (with jq)
- PowerShell
- Manual paste
# One-liner: authenticate + extract in one shot
ACCESS_TOKEN=$(curl -sX POST https://images.retaildigitals.com/api/v1/auth/token \
-H "Content-Type: application/json" \
-d "{\"client_id\":\"$RD_CLIENT_ID\",\"api_key\":\"$RD_API_KEY\"}" \
| jq -r .access_token)
echo "$ACCESS_TOKEN" # sanity check
$body = @{ client_id = $env:RD_CLIENT_ID; api_key = $env:RD_API_KEY } | ConvertTo-Json
$auth = Invoke-RestMethod -Method Post `
-Uri 'https://images.retaildigitals.com/api/v1/auth/token' `
-ContentType 'application/json' -Body $body
$env:ACCESS_TOKEN = $auth.access_token
export ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# ↑ paste just the value of "access_token" from the JSON response above
Quick sanity check — did the token work?
Before spending credits, verify with the free /balance endpoint:
curl -s https://images.retaildigitals.com/api/v1/balance \
-H "Authorization: Bearer $ACCESS_TOKEN"
You should get back a JSON body with your credit_balance. If you get a 401,
you've either pasted the JSON blob instead of just the token value, or the token
has already expired (unlikely — they live 1 hour).
Step 4 — Make your first product request
Now the fun part. Fetch a real product:
- curl
- JavaScript
- PHP
- Python
curl https://images.retaildigitals.com/api/v1/products/017000161563 \
-H "Authorization: Bearer $ACCESS_TOKEN"
const product = await fetch(
'https://images.retaildigitals.com/api/v1/products/017000161563',
{ headers: { Authorization: `Bearer ${auth.access_token}` } }
).then((r) => r.json());
console.log(product);
<?php
$ch = curl_init('https://images.retaildigitals.com/api/v1/products/017000161563');
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $auth['access_token']],
CURLOPT_RETURNTRANSFER => true,
]);
$product = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($product);
product = requests.get(
"https://images.retaildigitals.com/api/v1/products/017000161563",
headers={"Authorization": f"Bearer {auth['access_token']}"},
).json()
print(product)
Response:
{
"barcode": "017000161563",
"image_meta": {
"front": { "photo_date": "2026-06-04 15:49:50", "resolution": "1000x1000", "ownership": "Studio A" },
"front_clean": { "photo_date": "2026-06-04 15:49:50", "resolution": "1000x1000", "ownership": "Studio A" },
"back": null,
"back_clean": null
},
"product_meta": {
"brand": "Example Brand",
"product_name": "Example Cereal",
"category": "cereal",
"bracha": "mezonos",
"kosher_for_passover": false,
"gluten_free": false,
"vegan": true,
"hechshers": [
{ "symbol": "OU", "org": "Orthodox Union", "dairy_meat_pareve": "pareve" }
],
"nutrition": {
"serving_size": "1 cup (28g)",
"calories": 100,
"sodium": "200mg",
"total_carbohydrate": "24g"
},
"ingredients_text": "Milled corn, sugar, malt flavoring...",
"allergens": ["wheat"]
},
"images": {
"front": "https://images.retaildigitals.com/api/v1/image/eyJi...(base64url).(HMAC)",
"front_clean": "https://images.retaildigitals.com/api/v1/image/eyJi..."
},
"meta": {
"request_id": "req_01H8Y3G7Z8mnpqrsw",
"credits_debited": 2.6,
"credits_remaining": 7.4
}
}
🎉 You just made your first API call.
The images.front URL is signed, valid for 15 minutes, bound to the IP that requested it, and single-use (the first successful GET consumes it).
That has practical implications you need to design around:
curlfrom your terminal, then pasting into a browser — usually won't work. Your browser is on a different route (Wi-Fi vs VPN vs mobile hotspot) →403 signed_url_ip_mismatch.- Embedding directly in an
<img src>in an HTML page — works only when the same client (usually the browser) makes both the API call and fetches the image, AND you haven't fetched the URL from anywhere else first. - Server-side proxying — the safest pattern for web apps. Your server fetches the API, gets the signed URL, downloads the image bytes, caches them, and serves to end users from its own URL.
- You want a URL you can embed without server-side plumbing — use the 302-redirect endpoint instead:
GET /api/v1/products/{barcode}/image?variant=frontreturns a fresh signed URL bound to the caller's IP each request, so the URL you embed doesn't need to be pre-fetched.
Note meta.credits_debited in the response — that's the "everything, please"
default cost, and it's usually more than you need. The next step shows the two
levers to bring it down. Full cost model is on the Pricing page.
Step 5 — Fetch just what you need (control the metadata scopes)
The first cost lever is include=. Omitting it charges you both metadata scopes
(product_meta and image_meta) whether your app uses them or not. If you only need
images, ask only for images:
curl "https://images.retaildigitals.com/api/v1/products/017000161563?include=images" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Response (only images; no product_meta or image_meta blocks):
{
"barcode": "017000161563",
"images": {
"front": "https://images.retaildigitals.com/api/v1/image/eyJi...",
"front_clean": "https://images.retaildigitals.com/api/v1/image/eyJi..."
},
"meta": { "credits_debited": "...", "credits_remaining": "..." }
}
Check meta.credits_debited on the response — it dropped by exactly 2 × per_scope_rate
compared to the Step 4 default. That's the metadata-cost saving.
Available include values: image_meta, product_meta, images. Combine with commas,
or omit for all three (default — costs more).
Only pay for the image variants you actually use (the second cost lever)
Even with include=images, the API still returns URLs for every available variant
of the barcode (up to 4) and charges you per URL. If your app only ever displays the
front image, use variants= to filter:
curl "https://images.retaildigitals.com/api/v1/products/017000161563?include=images&variants=front" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Response — same shape, but only the variants you asked for get a signed URL. The others come back as null:
{
"barcode": "017000161563",
"images": {
"front": "https://images.retaildigitals.com/api/v1/image/eyJi...",
"back": null,
"front_clean": null,
"back_clean": null
},
"meta": { "credits_debited": "...", "credits_remaining": "..." }
}
Available values: front, back, front_clean, back_clean — comma-separated.
front_clean / back_clean are the transparent-background PNGs. Requesting a variant
that doesn't exist for the barcode returns null for that slot and doesn't cost anything.
Anything outside these four values returns 400 bad_request.
Sending variants=front without include=images still charges you the two default
metadata scopes on top of the one image (~2 × per_scope + 1 × per_image, not 1 × per_image).
You'll see this in meta.credits_debited — it'll be higher than you expected.
The minimum-cost image fetch is the combination ?include=images&variants=front.
This trap was the biggest source of surprise overspend in our first customer integration audit — see the Pricing page for the full formula and cheapest-pattern ladder.
Pro tip: If you only need to know whether a barcode exists — use HEAD for 0.05 credits instead of a full GET:
curl -I https://images.retaildigitals.com/api/v1/products/017000161563 \
-H "Authorization: Bearer $ACCESS_TOKEN"
Returns 200 OK if the barcode exists, 404 Not Found if it doesn't. No response body, no metadata cost.
Step 6 — Check your credit balance (free)
Every response includes your remaining credits in the meta block, but you can also fetch just the balance any time — great for dashboards, low-credit alerts, or a sanity-check before an expensive call.
curl https://images.retaildigitals.com/api/v1/balance \
-H "Authorization: Bearer $ACCESS_TOKEN"
Response:
{
"client_id": "rd_client_01h8y3g7z8mnpqrsw",
"credit_balance": 500.00,
"tier": "standard",
"meta": {
"request_id": "req_01H8Y3G7Z8mnpqrsw",
"credits_debited": 0,
"credits_remaining": 500.00,
"response_generated": "2026-07-02T16:30:00-04:00"
}
}
Prefer the full account snapshot — with rate-limit budget, usage-to-date, and scope info — via GET /account:
curl https://images.retaildigitals.com/api/v1/account \
-H "Authorization: Bearer $ACCESS_TOKEN"
Response:
{
"client_id": "rd_client_01h8y3g7z8mnpqrsw",
"client_name": "Prod POS backend",
"tier": "standard",
"credit_balance": 500.00,
"usage": {
"requests_today": 342,
"requests_this_month": 8421,
"credits_spent_today": 12.60,
"credits_spent_this_month": 421.30
},
"rate_limits": {
"requests_per_minute": 60,
"requests_per_day": 10000,
"remaining_this_minute": 58,
"remaining_today": 9658
},
"meta": { "request_id": "req_01H8Y3G7Z8...", "credits_debited": 0, "credits_remaining": 500.00 }
}
Both endpoints are free — 0 credits debited — and don't count against your daily quota differently than any other request. Safe to poll every few seconds from a status widget.
Where balance shows up:
credit_balanceon every/accountand/balanceresponse — current poolmeta.credits_remainingon every response (any endpoint) — same value, so you can update your local balance tracker after every call without extra requests- On the site:
https://images.retaildigitals.com/account/api-keysshows total credits across all your active API clients + spent-today and this-month counters
What's next
- Guides → Recipes for caching, bulk sync, POS integration, error retry strategies
- Authentication → Full token lifecycle, refresh flow, credential rotation, IP allowlisting
- API reference → Every endpoint documented — parameters, response schemas, error codes
- SDKs → Official libraries for JavaScript & PHP; community libraries for Python, Go
- Rate limits → Per-tier limits, how to read
X-RateLimit-*headers - Error handling → All error codes explained, retry strategy for each
Troubleshooting your first request
401 Unauthorized — "Invalid client credentials"
- Double-check that
client_idandapi_keyare both correct. Theapi_keyis case-sensitive. - If you've rotated the key since issuance, the old one is dead. Use the current one from Account → API Keys.
- If you can't find your
api_key, rotate to get a new one.
401 Unauthorized — "Access token expired"
Access tokens live 1 hour. Use your refresh_token to get a new one — see Authentication → Refresh flow. Or exchange your client_id + api_key again.
402 Payment Required — "Insufficient credits"
Your balance is empty. New accounts get 10 free credits; after that, top up at Billing.
404 Not Found — "Barcode not found"
The barcode isn't in our catalog. Try one of these known-good test barcodes:
017000161563— Example Brand Cereal030772095355— a national household-goods brand080878194964— a national snack brand
Or browse the full catalog with GET /api/v1/products.
429 Too Many Requests
You're hitting rate limits. Read the Retry-After header (in seconds) and back off. See Rate limits for per-tier limits. Enterprise-tier customers can request higher limits — email api@retaildigitals.com.
Signed image URL returns 410 Gone
Signed URLs are single-use. After the first successful GET, the URL is dead. This is a security feature — even if the URL leaks, it can't be used twice. Get a fresh signed URL by calling GET /api/v1/products/{barcode}?include=images again (only costs the delivery credits if you re-download the image).
Signed image URL returns 403 Forbidden
Signed URLs are IP-bound — they only work from the exact same IP that requested them. This is one of the load-bearing security layers and cannot be disabled — it's the trace-back-to-you protection if URLs ever leak. Fix the caller-side pattern:
- Fetch the image on the same server that generated the URL, then re-serve it from your own origin (recommended for most web apps).
- Use
GET /api/v1/products/{barcode}/image?variant=front— it returns a302 Location:to a freshly-issued signed URL bound to the caller's IP, so you can embed the redirect URL directly in<img src>without the browser first fetching then re-fetching. - If your topology genuinely requires CDN-topology signed URLs (Enterprise-tier),
contact
api@retaildigitals.comfor the CDN mode design.
Still stuck? Email api@retaildigitals.com — a human writes back within one business day.