Integration contract · agent-core Brain v2 → UAS

Sourcing request contract

How Brain v2 asks UAS to source a product, and how it reads back a value for every requested spec — per item, keyed by the exact spec name it sent.

POST/api/sourcing-jobs

Asynchronous. UAS returns 202 with a job_id immediately, sources in the background, and the Brain polls the result. The body is lenient — unknown fields never fail a job, and anything omitted falls back to the Firestore sr_context.

The three calls
01 · POST

Submit the job

POST /api/sourcing-jobs202 { job_id, resultUrl, statusUrl }. Optional Idempotency-Key header dedupes retries.

02 · POLL

Wait for the result

GET /api/sr/<sr_id>/result → poll until 200 (the ranked shortlist + stats).

03 · READ

Read per-item specs

GET /job/<job_id>/products → each product with a value for every spec, keyed by name.

Request body

required marks the only hard requirement. Everything else is optional and falls back to the Firestore sr_context.

FieldTypeDescription
sr_id string | number Sourcing-request id (coerced to string). Keys the persisted result + the resultUrl the Brain polls. required
sourcing_request object The v2 payload. Fields below are its properties.
productstringProduct type. Drives the product-type gate, the seed search keyword, and the fallback query.
descriptionstringFree-text requirements. Grounds spec structuring + search.
specsSpec[]The requirements to source against and return per item. Shape in the next table. Each spec's name round-trips verbatim to the output.
target_pricestring | nullPrice gate, e.g. "5 USD" / "0.2 CNY". Over-target products are withheld (not deleted) and reported, recoverable via the guide endpoint. null/omit = no gate.
moqnumber | nullTarget order quantity (alias for quantity). A product whose supplier MOQ exceeds it is kept but flagged (buffer_notes) — never dropped.
quantitynumber | nullSame as moq; if both are sent, quantity wins.
customizationboolean | stringA truthy value (or a description) marks the SR custom → suppliers who can't customise are dropped.
reference_imagesstring[]Product/concept reference image URLs. Used for reverse-image discovery + grounding visual specs (colour/shape/finish).
bump_versionbooleanOptional reserved flag; harmless if omitted.
The Spec object · sourcing_request.specs[]
FieldTypeDescription
spec stringThe spec name. Returned as spec_name byte-for-byte, so you read values back by it. required
valuestringThe customer's target / requirement (free prose). Normalized into discrete match values — "white, red, and black"["white","red","black"].
reasonstringWhy the spec matters (carried as the spec's reasoning seed).
sourcestringProvenance only (e.g. "customer") — NOT a match hint. UAS derives where to verify (text vs image) itself.
priorityenumimportant → veto hard filter   nice_to_have → rerank preference. Omit → defaults to nice_to_have.
Full example · SR 3947
// POST /api/sourcing-jobs   (comments are for docs — send valid JSON)
{
  "sr_id": "3947",
  "sourcing_request": {
    "product":      "3-color palette cake face paint kit with brush",
    "description":  "3-color face paint kit, white/red/black palette cakes, small brush, soap-and-water washable.",
    "target_price": "5 USD",        // price gate; over-target withheld + reported
    "moq":          500,            // target order quantity (alias for quantity)
    "customization":"front debossed logo",
    "reference_images": ["https://your-cdn/ref-1.jpg", "https://your-cdn/ref-2.jpg"],
    "specs": [
      { "spec": "product category", "value": "Halloween 3-color face paint kit", "source": "customer", "priority": "important" },
      { "spec": "colors included",  "value": "white, red, and black",            "source": "customer", "priority": "important" },
      { "spec": "brush included",   "value": "small brush included",             "source": "customer", "priority": "important" },
      { "spec": "paint format",     "value": "palette cakes",                    "source": "customer", "priority": "important" },
      { "spec": "washability",      "value": "soap-and-water washable",          "source": "customer", "priority": "important" },
      { "spec": "pack format",      "value": "plain retail box acceptable",      "source": "customer", "priority": "nice_to_have" }
    ]
  }
}

202 response: { "accepted": true, "job_id": "job_3947_…", "sr_id": "3947", "status": "processing", "statusUrl": "/api/runs/<job_id>", "resultUrl": "/api/sr/3947/result" }

Reading results
GET/job/<job_id>/products

Returns { products: ProcessedProduct[], total_count }. Each product's spec_matching_data carries veto_specs[] (your important specs) and rerank_specs[] (your nice_to_have specs) — every spec you sent, each in this shape:

FieldTypeDescription
spec_namestringYour input spec, byte-for-byte. Read values back by this.
product_valuestring | nullWhat UAS extracted from the listing (title, structured props, images). null when not found.
spec_valuesstring[]The customer target (normalized from your value).
match_typeenummatch not_match unknownunknown+null = not stated in the listing (a valid answer, never guessed).
reasoningstringEvidence / why (e.g. "desc image 4: spec table").
priorityenumimportant for veto specs, nice_to_have for rerank — mirrors your input.
"spec_matching_data": {
  "verdict": "pending",
  "veto_specs": [
    { "spec_name": "colors included", "spec_values": ["white","red","black"],
      "product_value": "white / red / black", "match_type": "match",
      "reasoning": "desc image 4: palette", "priority": "important" },
    { "spec_name": "washability", "spec_values": ["soap-and-water washable"],
      "product_value": null, "match_type": "unknown",
      "reasoning": "not stated in listing", "priority": "important" }
  ],
  "rerank_specs": [ /* pack format … */ ]
}

Query params: is_shortlisted=true (confirmed only) · include_eliminated=true (append cut products + why) · page / page_size · exclude_spec_matching_data=true · exclude_cleanup_data=true.

Per-product verdict
shortlisted

Every veto (important) spec confirmed match. The confirmed shortlist.

pending

No veto failed, but ≥1 veto is unknown. Still returned, with all values.

eliminated

A veto is not_match. Excluded from the shortlist; visible via include_eliminated=true.

Semantics that matter
One-shot curl
curl -sS -X POST https://dev.uas.sourcy.ai/api/sourcing-jobs \
  -H 'content-type: application/json' \
  -H 'idempotency-key: sr3947-run-1' \
  -d @request.json
# then poll:  curl -sS https://dev.uas.sourcy.ai/api/sr/3947/result
# then read:  curl -sS "https://dev.uas.sourcy.ai/job/<job_id>/products?is_shortlisted=false"