Quickstart
From zero to a structured shelf analysis. Assumes you have a tenant and a catalog loaded — if you don't, talk to us first.
Get credentials
The API authenticates with a Firebase ID token. Service-to-service callers use a service account; interactive clients use the signed-in user's token. Your tenant id scopes every path — a token issued for one tenant cannot read another's data.
Self-serve key issuance isn't live yet — this step currently routes through onboarding. Replace with the console flow at launch.
Upload the image
Bytes go to Cloud Storage, not to the API. Stamp the object's customMetadata.image_object_reference with the Firestore document path — that key is the only way the storage trigger can route the upload to the right document, so an object without it is unroutable.
Request an analysis
The call is asynchronous and idempotent per (image, model_version). Re-requesting an analysis that is already pending or running gives you back the existing job rather than queueing a second one.
curl -X POST \ "https://api.theshelfpro.com/v1/tenants/acme/tasks/t_8f21c4/images/img_44c9e0:analyze" \ -H "Authorization: Bearer $SHELFPRO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "firebase_storage_path": "tenants/acme/tasks/t_8f21c4/img_44c9e0.jpg", "priority": "high" }'{ "job_id": "job_7bd21f", "state": "pending", "tenant_id": "acme", "task_id": "t_8f21c4", "image_id": "img_44c9e0", "created_at": "2026-08-16T09:14:22.004Z", "started_at": null, "finished_at": null, "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "attempt": 1}Read the result
Poll the job, or — better — read the image document directly. Analysis is embedded on the image as image_analysis, so a realtime listener gets the result in the same snapshot as the image, with no join and no second subscription.
{ "width": 4032, "height": 3024, "model_name": "acme-detector", "model_version": "v1.4.0", "source": "server", "analyzed_at": "2026-08-16T09:14:24.118Z", "analysis_time": 0.68, "detected_objects": 13, "detected_products": 12, "superseded": false, "image_analysis_objects": [ { "normalized_x1": 0.147, "normalized_y1": 0.125, "normalized_x2": 0.273, "normalized_y2": 0.300, "detection_id": 0, "detection_class": "product", "detection_score": 0.981, "class_id": 40218, "class_name": "Gold 2L", "class_score": 0.976, "size": "2L", "corrected": false }, { "normalized_x1": 0.493, "normalized_y1": 0.412, "normalized_x2": 0.667, "normalized_y2": 0.587, "detection_id": 1, "detection_class": "out_of_stock", "detection_score": 0.913, "class_id": null, "class_name": null, "class_score": null, "corrected": false } ]}Handle the two object kinds
Every entry carries a detector result; only products carry a classifier result. An out_of_stock object is a gap on the shelf — it has a box and no class_id. Getting this wrong is the most common integration bug:
- Share-of-shelf denominators must include gaps.
- SKU counts must not.