Public API: Recommendations

The recommendations API has two parts under one resource:

  • Recipes: a recipe picks an algorithm (BESTSELLERS, TRENDING, NEW_ARRIVALS, VIEWED_TOGETHER, BOUGHT_TOGETHER, SIMILAR) and its tuning knobs. Each recipe owns one engine-managed output dataset that the snippet serves from the edge.
  • Merchandising rules: per-project (or per-recipe) overrides that boost, bury, pin, or exclude products in a recipe's output.

Both live under a project and authenticate with a service token. The org comes from the token, so every path below carries only {projectId}, not {orgId}. There is no single endpoint for "recommendations": each part has its own base path.

Plain text
https://app.avsb.cloud/api/v1/projects/{projectId}/recommendations/recipeshttps://app.avsb.cloud/api/v1/projects/{projectId}/recommendations/merchandising-rules
Plain text2 lines

Scopes

A scope is a named permission on your token. It controls exactly what the token can read or change.

OperationScope
List / get recipes and rulesrecommendations:read
Create / update / delete recipes and rulesrecommendations:write
Trigger an engine run (recipes/{recipeId}/run)recommendations:write

Every /api/v1 token is rate-limited: a scoped token gets 600 reads and 120 writes per minute, and an admin:* token gets 600 requests per minute, reads and writes together. Every write call below also accepts an optional Idempotency-Key header for safe retries. See Conventions for both.

Info

Every recommendations endpoint also needs the integrations_api_export plan feature, on top of its scope. Without it every call refuses with 403 feature_disabled and details.feature: "integrations_api_export", no matter which scope the token holds. See Refusals at 403.

Info

recipes/{recipeId}/run is a compute trigger. It rides the token's write rate-limit tier. On top of that, the engine enforces its own stricter limit: one manual run per recipe per 10 minutes.

List recipes

GET /api/v1/projects/{projectId}/recommendations/recipes: cursor-paginated, newest first, up to 100 per page (default 20). Requires recommendations:read.

curl 'https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/recipes?limit=20' \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": [    {      "id": "rec...",      "shortId": 3,      "projectId": "cmp...",      "name": "Homepage bestsellers",      "algorithm": "BESTSELLERS",      "params": { "windowDays": 28, "topN": 12 },      "fallbackChain": [],      "outputDatasetId": "cmd...",      "outputDatasetSlug": "homepage-bestsellers-engine",      "enabled": true,      "lastRunAt": "2026-06-18T02:00:00.000Z",      "lastTriggeredAt": null,      "lastRunStats": { "status": "success", "rows": 12 },      "lastRunStatus": "succeeded",      "lastRunError": null,      "createdAt": "2026-06-18T00:00:00.000Z",      "updatedAt": "2026-06-18T00:00:00.000Z"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON24 lines

Pass page.nextCursor back as ?cursor= to fetch the next page. Treat the cursor as an opaque string.

Run status

Every recipe carries lastRunStatus and lastRunError, so you can tell whether the last run worked without reading the stats yourself.

lastRunStatusMeaning
nullThe recipe has never been run.
queuedA run was triggered and the engine has not reported on it yet.
runningThe engine is working on it (the Similar items pipeline reports its stage while it runs).
succeededThe last run finished. If it found too little shopper activity to publish a new list, lastRunStats.status is gathering_data.
failedThe last run failed. lastRunError says why.

A run that is still queued 30 minutes after it was triggered turns into failed, with a lastRunError saying the engine did not report back. Trigger it again; if it keeps happening, contact support.

Create a recipe

POST /api/v1/projects/{projectId}/recommendations/recipes: name and algorithm are required. Creating a recipe also provisions its engine output dataset, 201 Created. Requires recommendations:write.

The smallest request that works:

curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/recipes \  -X POST -H "Authorization: Bearer avsb_svc_..." -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{ "name": "Homepage bestsellers", "algorithm": "BESTSELLERS" }'
Shell4 lines
Info

algorithm is immutable after creation; pick it once. Leaving out params uses the algorithm's engine defaults (windowDays: 28, topN: 12, minSupport: 3, topK: 12, all clamped 1 to a per-field cap). fallbackChain lets a recipe fall back to another recipe or dataset, up to 4 steps, when it returns no results for a seed.

Adding the optional fields sends a non-default topN and a fallbackChain that falls back to another recipe when this one has no results for a seed:

Fuller request
curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/recipes \  -X POST -H "Authorization: Bearer avsb_svc_..." -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \  -d '{    "name": "Homepage bestsellers",    "algorithm": "BESTSELLERS",    "params": { "windowDays": 28, "topN": 12 },    "fallbackChain": [{ "kind": "recipe", "id": "rec_fallback123" }]  }'
Shell8 lines
Response
{ "data": { "id": "rec...", "shortId": 4, "name": "Homepage bestsellers", "algorithm": "BESTSELLERS", "params": { "windowDays": 28, "topN": 12 }, "fallbackChain": [{ "kind": "recipe", "id": "rec_fallback123" }], "outputDatasetSlug": "homepage-bestsellers-engine", "enabled": false, "lastRunAt": null, "lastTriggeredAt": null, "lastRunStats": null, "lastRunStatus": null, "lastRunError": null, "...": "..." } }
JSON1 line

A new recipe starts with "enabled": false: nothing serves it until you turn it on with PATCH { "enabled": true } (see below). Turning on a SIMILAR recipe also starts its first run, so its results exist before the next nightly run.

topN caps at 50 (minSupport at 100, windowDays at 365, topK at 50); an out-of-range value is refused before it reaches the database:

400: topN out of range
{ "error": { "code": "validation_failed", "message": "Request body failed validation", "details": { "issues": [{ "param": "params.topN", "path": ["params", "topN"], "code": "too_big", "limit": 50, "expected": "number", "message": "Number must be less than or equal to 50" }] }, "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" } }
JSON1 line

Get / update / delete a recipe

GET, PATCH, and DELETE on /api/v1/projects/{projectId}/recommendations/recipes/{recipeId}. PATCH updates name, params, fallbackChain, and enabled (not algorithm). Same request shape as Create a recipe above; only the URL and method change per language.

cURL: update
curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/recipes/<recipeId> \  -X PATCH -H "Authorization: Bearer avsb_svc_..." -H "Content-Type: application/json" \  -d '{ "enabled": false }'
Shell3 lines
Response
{ "data": { "id": "rec...", "enabled": false, "...": "..." } }
JSON1 line

DELETE returns the deleted recipe's id, but refuses with 409 when another recipe's fallback chain still points at this one:

cURL: delete
curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/recipes/<recipeId> \  -X DELETE -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{ "data": { "id": "rec..." } }
JSON1 line
409: referenced by another recipe's fallback chain
{ "error": { "code": "dataset_state_conflict", "message": "Recipe is referenced in the fallback chain of \"Category page fallback\" and cannot be deleted", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#conflict-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" } }
JSON1 line

Trigger an engine run

POST /api/v1/projects/{projectId}/recommendations/recipes/{recipeId}/run: enqueues a single engine run. Returns 202 Accepted when the job is forwarded. Requires recommendations:write.

curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/recipes/<recipeId>/run \  -X POST -H "Authorization: Bearer avsb_svc_..." \  -H "Idempotency-Key: $(uuidgen)"
Shell3 lines
Response
{ "data": { "queued": true } }
JSON1 line
StatusMeaning
202Run forwarded to the engine (runs asynchronously). The recipe's lastRunStatus reads queued until the engine reports.
429The recipe was triggered within the last 10 minutes.
502The recommendation engine is temporarily unreachable; retry shortly.
429: triggered too recently
{ "error": { "code": "rate_limited", "message": "Recipe was already triggered recently. Try again in 480 seconds.", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#rate-limit-headers", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" } }
JSON1 line
502: engine unreachable
{ "error": { "code": "internal_error", "message": "Recommendation engine is unavailable, try again shortly", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#server-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" } }
JSON1 line

Run-completion webhook

The run itself is asynchronous. A webhook is an automatic HTTP request AvsB sends to your server when something happens, instead of you having to poll for it. When the engine finishes a run, AvsB sends one: recommendation.run.completed on success, or commerce.job_failed on failure. A failure event also carries kind: "REC_RUN" and a reason describing what went wrong.

recommendation.run.completed payload
{  "id": "del_clxyz123abc",  "event": "recommendation.run.completed",  "timestamp": "2026-06-18T02:00:04.000Z",  "source": { "type": "commerce", "id": "rec...", "name": "Recommendation run: Homepage bestsellers" },  "bypassedGate": false,  "kind": "REC_RUN",  "projectId": "cmp...",  "projectName": "Marketing Site",  "link": "https://app.avsb.cloud/projects/cmp.../commerce/recommendations/rec..."}
JSON11 lines
Warning

This is the same signing scheme every AvsB webhook destination uses, not one built just for recipes. Every delivery carries an X-AvsB-Signature: sha256=<hex> header, plus X-AvsB-Delivery-Id and X-AvsB-Timestamp. The signature is an HMAC-SHA256 of {deliveryId}.{timestamp}.{rawBody}, not the raw body alone, keyed by the destination's signing secret. Read Verifying signatures for the full recipe, including the 5-minute replay-tolerance check and a drop-in Node.js and Python example.

Merchandising rules

Merchandising rules re-rank a recipe's output. A rule has a kind and a match selector (skus, category, or a where field test):

KindEffectRequires
boostRank matched items higherweight (> 1, ≤ 1000)
buryRank matched items lowerweight (> 0, < 1)
pinFix matched items to a slotposition (0–100)
excludeRemove matched itemsNone

List merchandising rules

GET /api/v1/projects/{projectId}/recommendations/merchandising-rules: cursor-paginated, newest first, up to 100 per page (default 20). Pass ?recipeId=<recipeId> to narrow to one recipe's rules plus the project-wide rules. Requires recommendations:read.

curl 'https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/merchandising-rules?limit=20' \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": [    {      "id": "rule...",      "projectId": "cmp...",      "recipeId": null,      "kind": "boost",      "match": { "skus": ["SKU-1", "SKU-2"] },      "weight": 2,      "position": null,      "enabled": true,      "createdAt": "2026-06-18T00:00:00.000Z",      "updatedAt": "2026-06-18T00:00:00.000Z"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON17 lines

Create a merchandising rule

POST /api/v1/projects/{projectId}/recommendations/merchandising-rules: kind and match are required; weight or position are required depending on kind, per the table above. Returns the new rule, 201 Created. Requires recommendations:write.

curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/merchandising-rules \  -X POST -H "Authorization: Bearer avsb_svc_..." -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{ "kind": "boost", "match": { "category": "shoes" }, "weight": 2 }'
Shell4 lines
Response
{ "data": { "id": "rule...", "kind": "boost", "weight": 2, "...": "..." } }
JSON1 line

Pin an exact product to a slot instead, which uses position rather than weight:

Fuller request: pin
curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/merchandising-rules \  -X POST -H "Authorization: Bearer avsb_svc_..." -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \  -d '{ "recipeId": "rec...", "kind": "pin", "match": { "skus": ["SKU-1"] }, "position": 0 }'
Shell3 lines

Mixing a kind with the wrong field refuses before it reaches the database, for example pin sent with weight:

400: pin does not accept weight
{ "error": { "code": "validation_failed", "message": "Request body failed validation", "details": { "issues": [{ "param": "weight", "path": ["weight"], "code": "custom", "message": "pin does not accept weight" }] }, "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" } }
JSON1 line

Update / delete a rule

PATCH and DELETE on /api/v1/projects/{projectId}/recommendations/merchandising-rules/{ruleId}. Same request shape as Create a merchandising rule above; only the URL and method change per language. Requires recommendations:write.

cURL: update
curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/merchandising-rules/<ruleId> \  -X PATCH -H "Authorization: Bearer avsb_svc_..." -H "Content-Type: application/json" \  -d '{ "weight": 3 }'
Shell3 lines
cURL: delete
curl https://app.avsb.cloud/api/v1/projects/<projectId>/recommendations/merchandising-rules/<ruleId> \  -X DELETE -H "Authorization: Bearer avsb_svc_..."
Shell2 lines

DELETE returns the deleted rule's id:

Response
{ "data": { "id": "rule..." } }
JSON1 line

Next steps

Was this helpful?