Unified Agentic Sourcing · Integration · ← Architecture guide

Fetching spec-matching data by job_id

For concierge-bot or any service that has a UAS job_id and needs the per-product spec-matching verdict + scores. TL;DR: hit GET /job/<job_id>/products — it returns PP-API-compatible products, each with a spec_matching_data block. job_id == runId.

The endpoint

GET /job/:job_id/products use this

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
The job page is keyed by the run id. A 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

EnvBase URLNotes
devhttps://unified-agentic-sourcing-dev.onrender.comRender; sleeps after 15 min idle (first call may cold-start).
prodhttps://uas.sourcy.aiAzure VM. Read-only endpoints are public; the playground is disabled.
localhttp://localhost:8787bun 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.

POST /api/sourcing-jobs 202 { job_id, statusUrl, resultUrl } GET /job/<job_id>/products
# 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.

ParamTypeDefaultMeaning
is_shortlistedboolfalsetrue → 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_databoolfalseDrop spec_matching_data from each product (lighter payload).
exclude_cleanup_databoolfalseDrop cleanup_data — set this if you ONLY want spec-matching.
pageint11-based page index.
page_sizeintItems per page. Omit for all. total_count always reports the full set.
excludecsvBare item ids to skip (replacement rounds — "give me the next-best I haven't used").
include_eliminatedboolfalseAppend 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.

FieldTypeMeaning
verdictenumshortlisted all must-have vetoes passed · pending a veto couldn't be confirmed · eliminated a veto failed (only via include_eliminated).
veto_scorenumberUAS's combined ranking signal (veto + rerank). Higher = better. Primary sort key.
rerank_match_scorenumber0–1 — fraction of good-to-have (rerank) specs the product matched.
veto_specsarrayOne per must-have spec: { spec_name, spec_values, product_value, match_type, reasoning }. match_typematch / 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_specsstring[]Names of the good-to-have specs that matched (quick list).
rerank_specsarrayPer 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_specsstring[]?Present when some vetoes were unknown — the specs we couldn't confirm.
unconfirmed_fallbackbool?true only when this item is served because no confirmed match existed (pending fallback under is_shortlisted=true).
buffer_notesstring[]?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"]
}
Ranking: products come back ordered best-first. Sort by 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

GET/api/runs/:job_id/resultUAS-native

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.

GET/api/sr/:sr_id/resultby SR

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.

GET/dashboard/:job_idhuman

Visual run detail (reasoning trace, products, per-spec matching, eliminated pool). For people, not services.

Notes & gotchas

⏳ AsyncA job is 202 + poll. /job/:id/products 404s until the run is persisted (≈ when it finishes). Poll GET /api/runs/:idstate:"done" first, or just retry on 404.
🟰 id mappingjob_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 readsWant the smallest payload? ?exclude_cleanup_data=true keeps just spec_matching_data. The reverse exists too.
🛟 never starvedWith 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.
📈 orderingProducts are returned best-first; veto_score desc is the sort key, shortlisted above pending.
🔎 found valueBoth 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 idIn 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.