Skip to content
Getting started

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.

1

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.

2

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.

3

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"  }'
202Accepted — job enqueued
{  "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}
4

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    }  ]}
5

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.