The endpoint
Returns { products: ProcessedProduct[], total_count }.
Each product carries spec_matching_data (verdict + scores + per-veto results) and
cleanup_data (title, price, supplier, variants). This is the same contract agent-core's
getJobProducts(jobId) reads, so it's a drop-in for PP-API.
# Minimal: every product for a job, with spec_matching_data curl -s "https://unified-agentic-sourcing-dev.onrender.com/job/job_3845_1782284160733_4evqxq/products" | jq
job_id returned by
POST /api/sourcing-jobs (e.g. job_3845_1782284160733_4evqxq) is exactly what goes in
the path. 404 means that run isn't persisted (wrong id, or the job hasn't finished yet).Base URLs
| Env | Base URL | Notes |
|---|---|---|
| dev | https://unified-agentic-sourcing-dev.onrender.com | Render; sleeps after 15 min idle (first call may cold-start). |
| prod | https://uas.sourcy.ai | Azure VM. Read-only endpoints are public; the playground is disabled. |
| local | http://localhost:8787 | bun run src/http/server.ts |
No auth header is required today — these read endpoints are open. Don't put secrets in query strings.
Where job_id comes from
If your service kicks off the sourcing, it already gets the id back. A job is async: you get
202 immediately, then poll.
# 1) start a job (or it's started for you) — returns a job_id curl -s -X POST "https://unified-agentic-sourcing-dev.onrender.com/api/sourcing-jobs" \ -H "content-type: application/json" \ -d '{"sr_id":"3845","sourcing_context":{"product":"bath sponge"}}' | jq # → { "job_id":"job_3845_...", "statusUrl":"/api/runs/job_3845_...", "resultUrl":"/api/sr/3845/result" }
Already know the SR but not the job_id? Use GET /api/sr/<sr_id>/result (latest run for
that SR) — see Other endpoints. The returned record's runId is the job_id.
Query parameters
All optional. Booleans are the literal string true.
| Param | Type | Default | Meaning |
|---|---|---|---|
is_shortlisted | bool | false | true → confirmed (shortlisted) only. If none are confirmed, falls back to the pending set flagged unconfirmed_fallback so you're never starved. false → shortlisted + pending. |
exclude_spec_matching_data | bool | false | Drop spec_matching_data from each product (lighter payload). |
exclude_cleanup_data | bool | false | Drop cleanup_data — set this if you ONLY want spec-matching. |
page | int | 1 | 1-based page index. |
page_size | int | — | Items per page. Omit for all. total_count always reports the full set. |
exclude | csv | — | Bare item ids to skip (replacement rounds — "give me the next-best I haven't used"). |
include_eliminated | bool | false | Append veto-eliminated products (with the failed veto + reasoning) for underflow diagnosis — "why did nothing match, which veto to relax". |
Response shape
{
"total_count": 12, // full set size (ignores paging)
"products": [
{
"product_id": "1600697770593", // bare item id (platform prefix stripped)
"stage": "sourcing",
"spec_matching_data": { /* see below */ },
"cleanup_data": { /* title, price, supplier{member_id,seller_id,platform_login_id,shop_url}, variants[] */ }
}
]
}
product_id is the bare platform item id. The platform lives in cleanup_data.platform
(1688 | alibaba).
The spec_matching_data block
This is what you asked for — the matcher's per-product output.
| Field | Type | Meaning |
|---|---|---|
verdict | enum | shortlisted all must-have vetoes passed · pending a veto couldn't be confirmed · eliminated a veto failed (only via include_eliminated). |
veto_score | number | UAS's combined ranking signal (veto + rerank). Higher = better. Primary sort key. |
rerank_match_score | number | 0–1 — fraction of good-to-have (rerank) specs the product matched. |
veto_specs | array | One per must-have spec: { spec_name, spec_values, product_value, match_type, reasoning }. match_type ∈ match / unknown / not_match; product_value = the value found on the product for that spec (the "found value"; null if not retained), reasoning = the matcher's note. |
matched_rerank_specs | string[] | Names of the good-to-have specs that matched (quick list). |
rerank_specs | array | Per good-to-have spec, same shape as veto_specs (spec_name, spec_values, product_value, match_type, reasoning) — so you get the found value + reasoning for rerank specs too, not just the matched names. |
unconfirmed_veto_specs | string[]? | Present when some vetoes were unknown — the specs we couldn't confirm. |
unconfirmed_fallback | bool? | true only when this item is served because no confirmed match existed (pending fallback under is_shortlisted=true). |
buffer_notes | string[]? | Soft flags — slightly over target price / requested quantity. Surface to the user; weigh during selection. |
// example spec_matching_data { "verdict": "shortlisted", "veto_score": 0.82, "rerank_match_score": 0.6, "matched_rerank_specs": ["soft_material", "cartoon_print"], "veto_specs": [ { "spec_name": "product_type", "spec_values": ["bath sponge"], "product_value": "cartoon bath sponge", "match_type": "match", "reasoning": "title says bath sponge" }, { "spec_name": "for_children", "spec_values": ["for kids"], "product_value": null, "match_type": "unknown", "reasoning": "age group not stated" } ], "rerank_specs": [ { "spec_name": "soft_material", "spec_values": ["soft sponge"], "product_value": "soft EVA sponge", "match_type": "match", "reasoning": "soft material noted" } ], "unconfirmed_veto_specs": ["for_children"] }
veto_score desc if you re-rank
yourself. shortlisted always ranks above pending.curl recipes
Replace the host + job_id. Pipe to jq for readability.
Only spec-matching, only confirmed picks
curl -s "https://unified-agentic-sourcing-dev.onrender.com/job/JOB_ID/products?is_shortlisted=true&exclude_cleanup_data=true" | jq
Just the verdict + score per product
curl -s "https://unified-agentic-sourcing-dev.onrender.com/job/JOB_ID/products" \ | jq '.products[] | {id:.product_id, verdict:.spec_matching_data.verdict, score:.spec_matching_data.veto_score}'
Paginated
curl -s "https://unified-agentic-sourcing-dev.onrender.com/job/JOB_ID/products?page=1&page_size=10" | jq '.total_count'
Underflow diagnosis — see why products were cut
curl -s "https://unified-agentic-sourcing-dev.onrender.com/job/JOB_ID/products?include_eliminated=true&reason=underflow_diagnosis" \ | jq '.products[] | select(.spec_matching_data.verdict=="eliminated") | {id:.product_id, failed:[.spec_matching_data.veto_specs[]|select(.match_type=="not_match").spec_name]}'
Other endpoints
The full run record (camelCase, UAS-native): result.shortlist[],
possibleMatches[], priceExcluded[] — and each item's specResults[] carries the
full per-spec reasoning + found value (richer than veto_specs), plus stats and cost.
Use this for debugging / showing the "why" to a human.
Latest run for an SR when you have the sr_id but not the job_id.
The record's runId field is the job_id you can then feed to /job/:id/products.
Visual run detail (reasoning trace, products, per-spec matching, eliminated pool). For people, not services.
Notes & gotchas
| ⏳ Async | A job is 202 + poll. /job/:id/products 404s until the run is persisted (≈ when it finishes). Poll GET /api/runs/:id → state:"done" first, or just retry on 404. |
| 🟰 id mapping | job_id == runId. product_id in the response is the bare item id (the 1688:/alibaba: prefix is stripped); platform is in cleanup_data.platform. |
| 🍃 lean reads | Want the smallest payload? ?exclude_cleanup_data=true keeps just spec_matching_data. The reverse exists too. |
| 🛟 never starved | With is_shortlisted=true and zero confirmed matches, you still get the best pending items, each flagged unconfirmed_fallback:true — check that flag before treating them as confirmed. |
| 📈 ordering | Products are returned best-first; veto_score desc is the sort key, shortlisted above pending. |
| 🔎 found value | Both veto_specs and rerank_specs carry product_value (the value found on the product for that spec) + reasoning. product_value is null when the run didn't retain per-spec detail (older runs / step-by-step playground) — present for normal sourcing/customer-selection jobs. matched_rerank_specs stays as the quick matched-names list. |
| 🚫 not the supplier id | In cleanup_data.supplier, platform_login_id (1688 seller_login_id / Alibaba login_id) and seller_id are the seller's account/ids — distinct from the numeric member_id. They're omitted (not faked from member_id) when TMAPI didn't provide them. |
Source of truth: src/http/server.ts (route /job/:id/products) &
src/integration/processed-product.ts (buildJobProducts). If those change, regenerate this page.