openapi: 3.0.3 info: title: Retail Digitals Image API version: 1.3.0 summary: Kosher CPG product images and metadata over REST. description: | A REST API for studio-shot product photography (front / back, JPG / PNG-transparent) and structured metadata (brand, ingredients, hechshers, nutrition) for **25,000+ kosher CPG products**. Used by retailers, POS vendors, and marketplaces. ### Getting started 1. Create an account at https://images.retaildigitals.com/register 2. Issue an API client at https://images.retaildigitals.com/account/api-keys 3. Exchange your `client_id + api_key` for an `access_token` via `POST /auth/token` 4. Attach `Authorization: Bearer ` to every subsequent request See the [Getting Started guide](https://images.retaildigitals.com/developers/getting-started) for a complete 5-minute walkthrough. ### Response headers you can rely on Every `/api/v1/*` response — 2xx, 4xx, 5xx, HEAD, 302 — includes: | Header | Meaning | |---|---| | `X-Request-ID` | Correlation ID (same value as `meta.request_id` in the body). Log this and include it in every support ticket. | | `X-Credits-Debited` | Credits charged by this request (`0` for free endpoints). Read this on `HEAD` responses too. | | `X-Credits-Remaining` | Client balance after this request. Present on authenticated responses only. | | `X-RateLimit-Limit-Minute` / `X-RateLimit-Remaining-Minute` | Per-minute rate-limit bucket. | | `X-RateLimit-Limit-Day` / `X-RateLimit-Remaining-Day` | Per-day rate-limit bucket (authenticated endpoints only). | | `X-RateLimit-Reset` | Unix epoch second when the tightest bucket refills. | | `Content-Type: application/json; charset=utf-8` | UTF-8 explicit for Hebrew / kosher-terminology fields. | ### Roadmap items (not yet available) - Cursor-based pagination on `GET /products` — planned; current offset-based `page` / `per_page` remains supported - `Sunset` / `Deprecation` headers for future v1 → v2 migration — planned - Webhooks for product updates — planned ### Recently shipped - **`Idempotency-Key` header on `POST /products/bulk-check` and `POST /auth/revoke`** — see each endpoint's parameters. 24-hour cache. - **`updated_since` filter on `GET /products`** — pass an ISO date to fetch only products updated on or after that date. contact: name: Retail Digitals API Support email: api@retaildigitals.com url: https://images.retaildigitals.com/developers/support license: name: Proprietary — Commercial use requires paid credits url: https://images.retaildigitals.com/developers/terms servers: - url: https://images.retaildigitals.com/api/v1 description: Production security: - BearerAuth: [] tags: - name: Authentication description: Exchange credentials for access tokens; refresh; revoke. - name: Products description: Retrieve individual products or paginated catalog browsing. - name: Images description: Signed URL delivery for product image bytes. - name: Account description: Query your credit balance, usage, quotas. - name: Meta description: Service health, pricing, and other public metadata. paths: /auth/token: post: tags: [Authentication] summary: Exchange client credentials for an access token description: | Trade your long-lived `client_id + api_key` for a short-lived access token (1 hour) and a long-lived refresh token (90 days). This is the only endpoint that accepts the raw `api_key`. security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AuthTokenRequest' responses: '200': description: Tokens issued content: application/json: schema: $ref: '#/components/schemas/AuthTokenResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/InvalidCredentials' } '403': { $ref: '#/components/responses/ClientDisabled' } '429': { $ref: '#/components/responses/RateLimited' } /auth/refresh: post: tags: [Authentication] summary: Refresh an expired access token description: | Trade a `refresh_token` for a new `access_token` (and a new rotated `refresh_token`). Refresh tokens are **single-use** — the response includes a new refresh token; store it and discard the old one. Reusing an already-consumed refresh token revokes all outstanding refresh tokens for the client as a compromise-detection safeguard. security: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AuthRefreshRequest' responses: '200': description: Tokens refreshed content: application/json: schema: $ref: '#/components/schemas/AuthTokenResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': description: Refresh token invalid, expired, or reused content: application/json: schema: $ref: '#/components/schemas/Error' /auth/revoke: post: tags: [Authentication] summary: Revoke the current access + refresh token description: | Explicitly invalidate the caller's tokens. Useful for logout, session end, or when a token is suspected to have leaked. The client itself is not disabled — you can obtain new tokens by calling `/auth/token` again with your credentials. **Idempotent replay:** send `Idempotency-Key: <20-255 chars>` to make retries safe against network hiccups. Replaying the same key within 24 hours returns the cached response with `Idempotency-Replayed: 1` header. parameters: - $ref: '#/components/parameters/IdempotencyKey' responses: '204': { description: Tokens revoked } '401': { $ref: '#/components/responses/Unauthenticated' } '409': description: Idempotency key already used with a different request content: application/json: schema: { $ref: '#/components/schemas/Error' } /products/{barcode}: parameters: - $ref: '#/components/parameters/Barcode' - $ref: '#/components/parameters/Include' - $ref: '#/components/parameters/Variants' get: tags: [Products] summary: Retrieve a single product with metadata + signed image URLs description: | Fetch a full product record by barcode. Returns three independent metadata scopes, selectable via the `include=` query parameter: - **`image_meta`** — per-variant image facts (photo_date, resolution, ownership, size) - **`product_meta`** — brand, ingredients, hechshers, nutrition — the whole product record - **`images`** — signed URLs for each available image variant (15-min TTL, IP-bound, single-use) Use **`variants=`** to explicitly pick which image variants you want URLs for (`front`, `back`, `front_clean`, `back_clean`). When omitted, all available variants are returned. **Pricing is per requested variant** — asking for `variants=front` on a 4-variant product costs 1× `per_image_rate` instead of 4×, so specify what you actually need. See the [Product Metadata Reference](https://images.retaildigitals.com/developers/reference/product-metadata) for the complete schema. responses: '200': description: Product found content: application/json: schema: $ref: '#/components/schemas/ProductResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthenticated' } '402': { $ref: '#/components/responses/InsufficientCredits' } '403': { $ref: '#/components/responses/OutOfScope' } '404': { $ref: '#/components/responses/NotFound' } '429': { $ref: '#/components/responses/RateLimited' } head: tags: [Products] summary: Check whether a barcode exists (no body returned) description: | Availability check — returns `200` if the barcode exists in the catalog, `404` if not. No response body, no metadata cost — just `X-Credits-Debited: 0.05` in the response headers. Useful for POS autocomplete or bulk-existence checks under 1 barcode at a time. responses: '200': { description: Barcode exists } '401': { $ref: '#/components/responses/Unauthenticated' } '404': { description: Barcode not found } /products: get: tags: [Products] summary: List / search the catalog (paginated) description: | Paginated catalog listing with rich filters. Returns barcodes + variant availability only — for full metadata + images, call `GET /products/{barcode}` per barcode. Free of charge (no credit cost) to encourage discovery. parameters: - name: page in: query schema: { type: integer, minimum: 1, default: 1 } - name: per_page in: query schema: { type: integer, minimum: 1, maximum: 500, default: 50 } - name: min_digits in: query description: Minimum barcode length (mirrors the admin catalog `min_barcode_length` setting) schema: { type: integer, minimum: 1 } - name: max_digits in: query schema: { type: integer } - $ref: '#/components/parameters/UpdatedSince' - name: has_front in: query description: Filter to products with a `front` JPG variant available schema: { type: boolean } - name: has_front_clean in: query schema: { type: boolean } - name: has_back in: query schema: { type: boolean } - name: has_back_clean in: query schema: { type: boolean } - name: ownership in: query description: Filter by ownership tag (e.g. `"Studio A"`) schema: { type: string } - name: search in: query description: Barcode prefix search (e.g. `071662` matches all barcodes starting with 071662) schema: { type: string } responses: '200': description: Paginated list content: application/json: schema: $ref: '#/components/schemas/ProductListResponse' '401': { $ref: '#/components/responses/Unauthenticated' } '429': { $ref: '#/components/responses/RateLimited' } /products/bulk-check: post: tags: [Products] summary: Check existence of up to 500 barcodes in one request description: | Batch existence check — pass up to 500 barcodes, get back which exist and which variants each has. Priced at `products.bulk_check.per_barcode` per barcode checked (see `GET /pricing`). Ideal for nightly marketplace syncs — combine with `updated_since` on `GET /products` to drive incremental catalog sync. **Idempotent replay:** send `Idempotency-Key: <20-255 chars>` to make retries safe. Replaying the same key + same body within 24 hours returns the cached response (with `Idempotency-Replayed: 1` header) — no re-charging. Replaying with a different body returns `409 idempotency_key_mismatch`. parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BulkCheckRequest' responses: '200': description: Bulk check result (or replayed cached result) headers: Idempotency-Replayed: schema: { type: string, enum: ['1'] } description: Present with value "1" when the response is a cached replay of a prior Idempotency-Key hit. content: application/json: schema: $ref: '#/components/schemas/BulkCheckResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthenticated' } '402': { $ref: '#/components/responses/InsufficientCredits' } '409': description: Idempotency key already used with a different request body content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: 'idempotency_key_mismatch', message: 'The Idempotency-Key has been used before with a different request body. Use a fresh key for a new request, or replay the identical body.' } /products/{barcode}/image: parameters: - $ref: '#/components/parameters/Barcode' - name: variant in: query required: true schema: type: string enum: [front, back, front_clean, back_clean] get: tags: [Images] summary: 302 redirect to a fresh signed image URL description: | Convenience endpoint for use in ``. Returns `302 Found` with the `Location` header pointing at a fresh signed URL. The browser follows the redirect, delivering the image bytes. Charged at the same rate as `products.get` for image delivery. responses: '302': description: Redirect to signed URL headers: Location: schema: { type: string, format: uri } '401': { $ref: '#/components/responses/Unauthenticated' } '402': { $ref: '#/components/responses/InsufficientCredits' } '404': { description: Barcode + variant combination not available } /image/{token}: parameters: - name: token in: path required: true description: Signed HMAC-encoded token from a `products` response `images.*` URL schema: { type: string } get: tags: [Images] summary: Delivered signed URL destination description: | Serves the actual image bytes with correct `Content-Type` (`image/jpeg` or `image/png`). Signed URL rules: - **15 minute TTL** — token payload includes `exp` claim - **IP-bound** — must be requested from the same IP that received the URL - **Single-use** — after first successful `GET`, the `jti` is blacklisted - **Watermark applied inline** — global watermark + per-client traceable overlay This endpoint is not directly authenticated — the signed token proves authorization. security: [] responses: '200': description: Image bytes streamed content: image/jpeg: { schema: { type: string, format: binary } } image/png: { schema: { type: string, format: binary } } '403': description: IP mismatch or watermark policy violation '404': description: Token invalid or barcode+variant no longer exists '410': description: Token expired or already consumed (single-use protection) /account: get: tags: [Account] summary: Get current client's balance, quotas, usage description: | Free endpoint. Returns your client's credit balance, current rate-limit budget remaining, and usage counters (today + this month). responses: '200': description: Account snapshot content: application/json: schema: $ref: '#/components/schemas/AccountResponse' '401': { $ref: '#/components/responses/Unauthenticated' } /balance: get: tags: [Account] summary: Quick credit balance check description: | Free, minimal-payload shortcut of `GET /account`. Returns just the current credit balance and tier — nothing else. Use this for cheap high-frequency balance polling (dashboard widgets, pre-flight checks before expensive calls, low-balance alerts). Charged at 0 credits; does not affect rate limits differently from any other request. responses: '200': description: Balance snapshot content: application/json: schema: $ref: '#/components/schemas/BalanceResponse' example: 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' '401': { $ref: '#/components/responses/Unauthenticated' } '403': { $ref: '#/components/responses/ClientDisabled' } '429': { $ref: '#/components/responses/RateLimited' } /usage: get: tags: [Account] summary: Historical usage query description: Time-series usage data for the caller's client. Free. parameters: - name: from in: query schema: { type: string, format: date } - name: to in: query schema: { type: string, format: date } - name: granularity in: query schema: type: string enum: [hour, day, month] default: day responses: '200': description: Usage data content: application/json: schema: $ref: '#/components/schemas/UsageResponse' '401': { $ref: '#/components/responses/Unauthenticated' } /health: get: tags: [Meta] summary: Service health check description: No auth required. Returns 200 if the API is operational. security: [] responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/HealthResponse' /pricing: get: tags: [Meta] summary: Current per-action credit prices description: | Snapshot of the current pricing table. Prices can change (rarely) at admin discretion — we announce changes in the changelog with 30 days notice. security: [] responses: '200': description: Pricing table content: application/json: schema: $ref: '#/components/schemas/PricingResponse' components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: | Access token obtained from `POST /auth/token`. Attach as `Authorization: Bearer `. Token is a signed JWT (HS256), lives 1 hour. Use `POST /auth/refresh` to renew. parameters: Barcode: name: barcode in: path required: true description: Product barcode (UPC-A, EAN-13, etc). Leading zeros preserved. schema: { type: string, pattern: '^[0-9]+$', minLength: 8, maxLength: 14, example: '017000161563' } Include: name: include in: query description: | Comma-separated list of metadata scopes to return. Valid values: `product_meta`, `image_meta`, `images`. **Omit → all three (default).** **Pricing is per scope** — each of `product_meta` and `image_meta` costs one `products.get` rate (admin-set — see [`GET /pricing`](#tag/Meta/paths/~1pricing/get)). `images` charges one `products.get.per_image` rate per URL returned, which the `variants=` parameter controls. **The default (omitting `include=`) charges the metadata cost for both `product_meta` and `image_meta` on top of the image cost, whether or not your integration uses them.** Set `include=` explicitly to skip metadata scopes you don't render. The cheapest read patterns: - `?include=product_meta` — text-only, 1× `products.get` rate - `?include=images&variants=front` — one image URL, 1× `products.get.per_image` rate - No params — all three scopes × all available variants (highest cost) Rates are admin-adjustable — always fetch [`GET /pricing`](#tag/Meta/paths/~1pricing/get) at daemon startup rather than hard-coding. See the [Pricing page](https://images.retaildigitals.com/developers/pricing) for the full formula and worked patterns. schema: type: string example: 'images' IdempotencyKey: name: Idempotency-Key in: header required: false description: | Client-generated idempotency token. 20-255 chars, URI-unreserved (`A-Za-z0-9._~-`). Replaying the same key + same request body within 24 hours returns the cached response with an `Idempotency-Replayed: 1` header — safe for retries during network hiccups. Replaying with a different body returns `409 idempotency_key_mismatch`. Recommended: use a UUID per logical operation. schema: type: string minLength: 20 maxLength: 255 pattern: '^[A-Za-z0-9._~-]+$' example: 'idem_01h8y3g7z8mnpqrsw_2026-07-02' UpdatedSince: name: updated_since in: query required: false description: | Only return products whose `updated_at` is on or after this timestamp. Ideal for incremental catalog sync — save the current time before a sync, use it as `updated_since` on the next run to fetch just what changed. ISO date or datetime format. schema: type: string format: date-time example: '2026-07-01T00:00:00Z' Variants: name: variants in: query description: | Comma-separated list of image variants to include (`front`, `back`, `front_clean`, `back_clean`). **Omit to return every variant available for the barcode** — up to 4 URLs at the `products.get.per_image` rate each. **Pricing is per requested variant.** Combine with `include=images` to skip metadata scopes you don't need — otherwise you'll still pay the default metadata cost on top. Invalid values return `400 bad_request` with 0 credits charged. Current per-variant rate lives at [`GET /pricing`](#tag/Meta/paths/~1pricing/get) under `actions["products.get.per_image"]`. schema: type: string example: 'front' responses: BadRequest: description: Malformed request content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: 'bad_request', message: 'Missing required field: client_id' } Unauthenticated: description: Missing or invalid access token content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: 'unauthenticated', message: 'Access token expired' } InvalidCredentials: description: client_id + api_key rejected content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: 'invalid_credentials' } ClientDisabled: description: Client has been revoked or expired content: application/json: schema: { $ref: '#/components/schemas/Error' } InsufficientCredits: description: Not enough credits to charge content: application/json: schema: { $ref: '#/components/schemas/Error' } example: { error: 'insufficient_credits', message: 'Need 2.6 credits; balance is 0.4' } OutOfScope: description: This client is not scoped to see the requested barcode content: application/json: schema: { $ref: '#/components/schemas/Error' } NotFound: description: Barcode not in catalog content: application/json: schema: { $ref: '#/components/schemas/Error' } RateLimited: description: Too many requests headers: Retry-After: schema: { type: integer, description: 'Seconds to wait before retrying' } content: application/json: schema: { $ref: '#/components/schemas/Error' } schemas: AuthTokenRequest: type: object required: [client_id, api_key] properties: client_id: { type: string, example: 'rd_client_01h8y3g7z8mnpqrsw' } api_key: { type: string, example: 'sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' } AuthRefreshRequest: type: object required: [refresh_token] properties: refresh_token: { type: string, example: 'rt_01h8y3g7z8mnpqrsw...' } AuthTokenResponse: type: object properties: access_token: { type: string, description: 'JWT to attach as Bearer token' } refresh_token: { type: string, description: 'Opaque single-use refresh token' } token_type: { type: string, example: 'Bearer' } expires_in: { type: integer, description: 'Seconds until access token expires', example: 3600 } ProductResponse: type: object properties: barcode: { type: string, example: '017000161563' } image_meta: nullable: true $ref: '#/components/schemas/ImageMetaBlock' product_meta: nullable: true $ref: '#/components/schemas/ProductMetaBlock' images: nullable: true $ref: '#/components/schemas/ImagesBlock' meta: $ref: '#/components/schemas/ResponseMeta' ImageMetaBlock: description: Facts about the physical image files (dates, resolution, ownership). type: object properties: front: { $ref: '#/components/schemas/ImageMetaEntry' } back: { $ref: '#/components/schemas/ImageMetaEntry' } front_clean: { $ref: '#/components/schemas/ImageMetaEntry' } back_clean: { $ref: '#/components/schemas/ImageMetaEntry' } ImageMetaEntry: nullable: true description: null when this variant doesn't exist for the product type: object properties: photo_date: { type: string, format: date, description: 'When the shutter fired', example: '2023-10-19' } upload_date: { type: string, format: date-time, description: 'When landed on server (server-local UTC-4)' } resolution: { type: string, example: '1000x1000' } ownership: { type: string, example: 'Studio A' } uploaded_by: { type: string, nullable: true, example: 'Photographer 1' } file_size_bytes: { type: integer, example: 184521 } source_batch: type: string example: 'zip_upload' description: | Import-batch tag. Two stable values customers can rely on: `zip_upload` (the standard admin bulk-upload path) and `review_approved` (approved via the pending-review queue). Additional values may appear for one-off operational batches (e.g. legacy migrations); treat this as an open string, not a closed enum. ProductMetaBlock: description: Product-level metadata — brand, ingredients, hechshers, nutrition. type: object properties: brand: { type: string, nullable: true, example: "Example Brand" } product_name: { type: string, nullable: true } product_description: { type: string, nullable: true } net_weight: { type: string, nullable: true, example: 'Net Wt 20 oz (1 lb 4 oz / 567 g)' } category: { type: string, nullable: true, example: 'frozen' } country_of_origin: { type: string, nullable: true, example: 'United States' } manufacturer: { type: string, nullable: true } distributor: { type: string, nullable: true } additional_certifications: { type: string, nullable: true } bracha: type: string nullable: true enum: [hamotzi, mezonos, hagafen, haetz, haadama, shehakol] kosher_for_passover: { type: boolean } gluten_free: { type: boolean } vegan: { type: boolean } organic: { type: boolean } hechshers: type: array items: { $ref: '#/components/schemas/Hechsher' } ingredients: { $ref: '#/components/schemas/Ingredients' } nutrition: nullable: true $ref: '#/components/schemas/Nutrition' notes: { type: string, nullable: true } scanned_at: { type: string, format: date-time } updated_at: { type: string, format: date-time } Hechsher: type: object properties: symbol: { type: string, example: 'OK' } org: { type: string, nullable: true, example: 'OK Kosher Certification' } logo: { type: string, nullable: true, example: 'ok.png' } dairy_meat_pareve: type: string nullable: true enum: [dairy, meat, pareve] additional_designation: type: string nullable: true example: 'Pas Yisroel' description: | Free-text designations layered on top of the base certification. Common values: Cholov Yisroel, Pas Yisroel, Yoshon, Bishul Yisroel, Non-Gebrokts, Kosher for Passover. Ingredients: type: object properties: ingredients_text: { type: string, nullable: true } contains_statement: { type: string, nullable: true, example: 'Contains: Fish (Pollock), Wheat.' } may_contain_statement: { type: string, nullable: true } allergens: type: array items: type: string example: 'wheat' description: | Normalized allergen names. Common values: milk, eggs, fish, shellfish, tree nuts, peanuts, wheat, soy, sesame, sulfites, gluten, crustacean shellfish, mollusks. Nutrition: type: object properties: servings_per_container: { type: string, nullable: true, example: 'About 5' } serving_size: { type: string, nullable: true, example: '4 sticks (112g)' } calories: { type: string, nullable: true, example: '230' } total_fat: { $ref: '#/components/schemas/NutritionEntry' } saturated_fat: { $ref: '#/components/schemas/NutritionEntry' } trans_fat: { $ref: '#/components/schemas/NutritionEntry' } cholesterol: { $ref: '#/components/schemas/NutritionEntry' } sodium: { $ref: '#/components/schemas/NutritionEntry' } total_carbohydrate: { $ref: '#/components/schemas/NutritionEntry' } dietary_fiber: { $ref: '#/components/schemas/NutritionEntry' } total_sugars: { $ref: '#/components/schemas/NutritionEntry' } added_sugars: { $ref: '#/components/schemas/NutritionEntry' } protein: { $ref: '#/components/schemas/NutritionEntry' } vitamin_d: { $ref: '#/components/schemas/NutritionEntry' } calcium: { $ref: '#/components/schemas/NutritionEntry' } iron: { $ref: '#/components/schemas/NutritionEntry' } potassium: { $ref: '#/components/schemas/NutritionEntry' } NutritionEntry: type: object properties: amount: { type: string, nullable: true, example: '8g' } dv: { type: string, nullable: true, description: '% Daily Value', example: '10%' } ImagesBlock: description: Signed URLs for each available variant. null when variant doesn't exist. type: object properties: front: { type: string, format: uri, nullable: true } back: { type: string, format: uri, nullable: true } front_clean: { type: string, format: uri, nullable: true } back_clean: { type: string, format: uri, nullable: true } ResponseMeta: type: object required: [request_id, credits_debited, credits_remaining, response_generated] properties: request_id: { type: string, example: 'req_01H8Y3G7Z8mnpqrsw' } credits_debited: { type: number, format: float, example: 4.6 } credits_remaining: { type: number, format: float, example: 4321.8 } response_generated: { type: string, format: date-time } ProductListResponse: type: object properties: data: type: array items: type: object properties: barcode: { type: string } variants: type: array items: type: string enum: [front, back, front_clean, back_clean] pagination: type: object properties: page: { type: integer } per_page: { type: integer } total: { type: integer } total_pages: { type: integer } meta: { $ref: '#/components/schemas/ResponseMeta' } BulkCheckRequest: type: object required: [barcodes] properties: barcodes: type: array minItems: 1 maxItems: 500 items: { type: string } BulkCheckResponse: type: object properties: results: type: object additionalProperties: type: object properties: exists: { type: boolean } variants: type: array items: { type: string } meta: { $ref: '#/components/schemas/ResponseMeta' } AccountResponse: type: object properties: client_id: { type: string } client_name: { type: string } tier: { type: string, enum: [free, standard, enterprise] } credit_balance: { type: number, format: float, example: 4327.4 } usage: type: object properties: requests_today: { type: integer } requests_this_month: { type: integer } credits_spent_today: { type: number, format: float } credits_spent_this_month: { type: number, format: float } rate_limits: type: object properties: requests_per_minute: { type: integer } requests_per_day: { type: integer } remaining_this_minute: { type: integer } remaining_today: { type: integer } scope: { type: object, description: 'Barcode scope this client can access' } BalanceResponse: type: object description: | Minimal shortcut response from `GET /balance` — only the fields you need for a cheap balance-poll widget. For full details, use `GET /account`. required: [client_id, credit_balance, tier, meta] properties: client_id: { type: string, example: 'rd_client_01h8y3g7z8mnpqrsw' } credit_balance: type: number format: float description: Credits remaining on this client example: 500.00 tier: type: string enum: [free, standard, enterprise] example: standard meta: { $ref: '#/components/schemas/ResponseMeta' } UsageResponse: type: object properties: data: type: array items: type: object properties: timestamp: { type: string, format: date-time } endpoint: { type: string } request_count: { type: integer } credits_spent: { type: number, format: float } error_count: { type: integer } meta: { $ref: '#/components/schemas/ResponseMeta' } HealthResponse: type: object properties: status: { type: string, enum: [ok, degraded, down] } version: { type: string, example: '1.0.0' } timestamp: { type: string, format: date-time } PricingResponse: type: object description: | The live pricing model. Every `GET /products/{barcode}` charges: `credits = (per_scope_rate × metadata_scopes_returned) + (per_image_rate × image_variants_returned)` The `actions` map is the raw per-unit rates from the DB; `descriptions` explains what each key charges for. Read `notes` for the cheapest request patterns. required: [currency, pricing_model, formula, actions] properties: currency: { type: string, example: credits } pricing_model: type: string example: per_scope_plus_per_variant description: Identifier of the current pricing scheme formula: type: string example: 'credits_per_get = (0.5 × metadata_scopes_returned) + (1.0 × image_variants_returned)' description: Plain-language formula for the compound cost of GET /products/{barcode} notes: type: array description: Cheapest-usage guidance for common patterns items: { type: string } example: - 'GET /products/{barcode}?include=images&variants=front is the cheapest way to fetch one image URL: 0 metadata scopes × 0.5 + 1 image × 1.0 = 1.0 credit.' - 'Omitting include= defaults to ALL THREE scopes (product_meta + image_meta + images), which adds 1.0 credit of metadata cost on top of the image count.' - 'Cheapest text-only sync: GET /products/{barcode}?include=product_meta (0.5 cr).' actions: type: object description: Per-unit rates from the DB (safe to fetch and cache) additionalProperties: { type: number, format: float } example: products.get: 0.5 products.get.per_image: 1.0 products.head: 0.05 products.list: 0 products.bulk_check.per_barcode: 0.01 image.get: 1 account.get: 0 descriptions: type: object description: Human-readable per-action explanations. Read these to understand what each `actions` rate covers. additionalProperties: { type: string } example: products.get: 'Per metadata scope returned. include=product_meta charges 0.5; include=image_meta charges 0.5; add both = 1.0. Independent of image count.' products.get.per_image: 'Per signed image URL returned. Use ?variants=front (etc) to limit which URLs are returned and pay only for what you display.' Error: type: object required: [error] properties: error: type: string description: Stable error code — safe to switch on in client code example: 'invalid_credentials' message: type: string description: Human-readable message; may change wording, don't switch on request_id: type: string example: 'req_01H8Y3G7Z8mnpqrsw'