Products
The catalog of what you sell when it is not vehicles, and its publication to your channels.
Download the full API pública projection: openapi.json.
| Method | Path | What it does |
|---|---|---|
GET | /products | Search products |
POST | /products | Create a product |
DELETE | /products/{id} | Delete a product |
GET | /products/{id} | Fetch one product |
PUT | /products/{id} | Update a product |
GET | /products/{id}/publications | Where this product is listed |
DELETE | /products/{id}/publications/{pubId} | Take a listing down |
POST | /products/{id}/publish | Publish to one or more portals |
POST | /products/{id}/variants | Add a variant to a product |
DELETE | /products/{id}/variants/{variantId} | Delete a variant |
PATCH | /products/{id}/variants/{variantId} | Update a variant |
GET | /products/publications | Publication state for many products at once |
GET /products
Search products
q is matched WORD BY WORD, accent- and case-insensitively: every word must appear in the SEARCH INDEX (title, SKU, brand, model — not description) or be an option value of one of the product’s active variants, with Spanish gender/plural folded (negra finds Negro). Question words and option names (talla, color, hay) are ignored, so polera negra talla M finds the polera that has a Negro × M variant.
Each row carries origin (native | shopify | woocommerce), external_id, and variants[] — every variant with its own sku, price_clp (null = the product’s price), available_quantity, available and a display label. A product’s available_quantity is the sum of its active variants’ stock when it has variants.
Read available_quantity is what can be SOLD now: on_hand_quantity (the counted units, what a stock sync writes) minus held_quantity (units kept 15 minutes for unpaid storefront checkouts, POST /orders/checkout).
Two defaults worth stating because they differ. active defaults to true — inactive products are hidden unless you ask for them. in_stock defaults to false, so products with available_quantity: 0 ARE included; pass in_stock=true for the sellable-today list. sort defaults to price_asc.
The response carries rows and nothing else — there is no total, so paginate by asking for limit + 1 rather than by counting.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
q | query | string | no | |
brand | query | string | no | |
min_price | query | number | null | no | |
max_price | query | number | null | no | |
condition | query | new \ | used | no |
active | query | any | no | |
in_stock | query | any | no | |
sort | query | price_asc \ | price_desc \ | recent |
limit | query | integer | no | ≥ 1, ≤ 100 |
offset | query | integer | null | no | ≥ 0 |
curl https://api.vitrinadev.com/api/v1/products \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "7a7a7a7a-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"origin": "native",
"external_id": null,
"title": "Filtro de aceite Ejemplo FA-100",
"sku": "FA-100",
"brand": "Ejemplo",
"model": "FA-100",
"category_hint": "Filtros de aceite",
"category_id": null,
"price_amount": 12990,
"price_clp": 12990,
"price_currency": "CLP",
"available_quantity": 14,
"condition": "new",
"listing_type": null,
"description": "Filtro de aceite para motores bencineros de 1.4 a 1.6 litros.",
"photos": [
{
"url": "https://cdn.example.cl/productos/fa-100-1.jpg",
"order": 0
}
],
"attributes": {
"rosca": "M20x1.5"
},
"tags": [
"filtros",
"mantencion"
],
"searchable_text": "filtro de aceite ejemplo fa-100 fa-100 ejemplo fa-100",
"active": true,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-01T14:20:00.000Z",
"variants": []
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /products
Create a product
Every field is optional — a product with no title is creatable. Validation of what a given PORTAL requires happens at publish time, not here, so a product can be stored complete enough for the agent to quote and still be rejected by Mercado Libre.
Pass variants[] to create the product WITH its variants in one transaction (3 tallas × 2 colores = 6 variants, each with its own options, sku, price_amount and available_quantity). With variants the stock lives on them, so a product-level available_quantity is refused (400). Products created here are origin: "native"; store products arrive through their sync.
Defaults: available_quantity: 0 (so a new product is out of stock until you say otherwise), condition: "new", price_currency: "CLP", active: true. category_hint is free text that drives the portal’s category prediction; set category_id only to override the predictor outright. Answers 201.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
title | string | null | — | máx. 255 |
sku | string | null | — | máx. 120 |
brand | string | null | — | máx. 120 |
model | string | null | — | máx. 120 |
category_hint | string | null | — | máx. 255 |
category_id | string | null | — | máx. 60 |
price_amount | number | null | — | ≥ 0 |
price_currency | string | — | máx. 8 |
available_quantity | integer | — | ≥ 0 |
condition | new \ | used | — |
listing_type | string | null | — | máx. 30 |
description | string | null | — | |
photos | object[] | — | |
attributes | object | — | |
tags | string[] | — | |
active | boolean | — | |
variants | object[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/products \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Filtro de aceite Ejemplo FA-100",
"sku": "FA-100",
"brand": "Ejemplo",
"model": "FA-100",
"category_hint": "Filtros de aceite",
"price_amount": 12990,
"price_currency": "CLP",
"available_quantity": 14,
"condition": "new",
"description": "Filtro de aceite para motores bencineros de 1.4 a 1.6 litros.",
"photos": [
{
"url": "https://cdn.example.cl/productos/fa-100-1.jpg",
"order": 0
}
],
"attributes": {
"rosca": "M20x1.5"
},
"tags": [
"filtros",
"mantencion"
]
}'Example response (201)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"origin": "native",
"external_id": null,
"title": "Filtro de aceite Ejemplo FA-100",
"sku": "FA-100",
"brand": "Ejemplo",
"model": "FA-100",
"category_hint": "Filtros de aceite",
"category_id": null,
"price_amount": 12990,
"price_clp": 12990,
"price_currency": "CLP",
"available_quantity": 14,
"condition": "new",
"listing_type": null,
"description": "Filtro de aceite para motores bencineros de 1.4 a 1.6 litros.",
"photos": [
{
"url": "https://cdn.example.cl/productos/fa-100-1.jpg",
"order": 0
}
],
"attributes": {
"rosca": "M20x1.5"
},
"tags": [
"filtros",
"mantencion"
],
"searchable_text": "filtro de aceite ejemplo fa-100 fa-100 ejemplo fa-100",
"active": true,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-01T14:20:00.000Z",
"variants": []
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /products/{id}
Delete a product
A hard delete of the canonical row; its publication records cascade away with it.
It does not take the listings down. Nothing here calls the portal, so a published product deleted this way stays live and purchasable on Mercado Libre while the local record that knew its external id is gone — leaving no handle to remove it with afterwards. DELETE /products/\{id\}/publications/\{pubId\} on each portal FIRST, then delete the product.
Its variants are deleted with it. A synced product belongs to its store and answers 409.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/products/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /products/{id}
Fetch one product
The canonical row, including photos, attributes and tags. Portal listing state is NOT here — read GET /products/\{id\}/publications for that.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/products/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"origin": "native",
"external_id": null,
"title": "Filtro de aceite Ejemplo FA-100",
"sku": "FA-100",
"brand": "Ejemplo",
"model": "FA-100",
"category_hint": "Filtros de aceite",
"category_id": null,
"price_amount": 12990,
"price_clp": 12990,
"price_currency": "CLP",
"available_quantity": 14,
"condition": "new",
"listing_type": null,
"description": "Filtro de aceite para motores bencineros de 1.4 a 1.6 litros.",
"photos": [
{
"url": "https://cdn.example.cl/productos/fa-100-1.jpg",
"order": 0
}
],
"attributes": {
"rosca": "M20x1.5"
},
"tags": [
"filtros",
"mantencion"
],
"searchable_text": "filtro de aceite ejemplo fa-100 fa-100 ejemplo fa-100",
"active": true,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-01T14:20:00.000Z",
"variants": []
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /products/{id}
Update a product
A PUT that behaves as a PATCH: only the fields present are applied and at least one is required. photos, attributes and tags are REPLACED wholesale when present, never merged.
Sending null does not clear a field. Nulls are dropped on the way through, so a field can be set and changed but not emptied here.
Send title, sku, brand and model together or not at all. Touching any one of the four rebuilds the search index from the four values IN THIS REQUEST, so patching just the title leaves a product that is no longer findable by its SKU or brand until they are written again. Other fields do not disturb the index.
This updates the canonical row only — live portal listings keep the old values until you POST /products/\{id\}/publish again.
A synced product (origin shopify / woocommerce) answers 409 for the fields its store owns (title, description, sku, brand, price_amount, price_currency, available_quantity, photos, tags, active) — the next sync would overwrite them; edit them in the store. Vitrina-only fields stay editable. A product with variants refuses available_quantity (400): edit the variant instead.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
title | string | null | — | máx. 255 |
sku | string | null | — | máx. 120 |
brand | string | null | — | máx. 120 |
model | string | null | — | máx. 120 |
category_hint | string | null | — | máx. 255 |
category_id | string | null | — | máx. 60 |
price_amount | number | null | — | ≥ 0 |
price_currency | string | — | máx. 8 |
available_quantity | integer | — | ≥ 0 |
condition | new \ | used | — |
listing_type | string | null | — | máx. 30 |
description | string | null | — | |
photos | object[] | — | |
attributes | object | — | |
tags | string[] | — | |
active | boolean | — |
curl -X PUT https://api.vitrinadev.com/api/v1/products/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Filtro de aceite Ejemplo FA-100 (pack x2)",
"sku": "FA-100",
"brand": "Ejemplo",
"model": "FA-100",
"available_quantity": 20
}'Example response (200)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"origin_integration_id": null,
"source": "manual",
"origin": "native",
"external_id": null,
"title": "Filtro de aceite Ejemplo FA-100 (pack x2)",
"sku": "FA-100",
"brand": "Ejemplo",
"model": "FA-100",
"category_hint": "Filtros de aceite",
"category_id": null,
"price_amount": 12990,
"price_clp": 12990,
"price_currency": "CLP",
"available_quantity": 20,
"condition": "new",
"listing_type": null,
"description": "Filtro de aceite para motores bencineros de 1.4 a 1.6 litros.",
"photos": [
{
"url": "https://cdn.example.cl/productos/fa-100-1.jpg",
"order": 0
}
],
"attributes": {
"rosca": "M20x1.5"
},
"tags": [
"filtros",
"mantencion"
],
"searchable_text": "filtro de aceite ejemplo fa-100 fa-100 ejemplo fa-100",
"active": true,
"published_at": null,
"last_seen_at": null,
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-03T10:05:00.000Z",
"variants": []
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /products/{id}/publications
Where this product is listed
One row per portal the product has been pushed to, carrying status, the portal’s external_id, last_published_at and last_error.
A row with status: "deleted" is a TOMBSTONE, not a live listing — it records that the product was once published there and has since been taken down. Count only what you mean to.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/products/<id>/publications \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "7a7a7a7a-0000-4000-8000-000000000011",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"product_id": "7a7a7a7a-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC1234567890",
"status": "published",
"last_synced_at": "2026-10-02T09:15:00.000Z",
"last_published_at": "2026-10-02T09:15:00.000Z",
"last_error": null,
"raw_provider_payload": null,
"created_at": "2026-10-02T09:15:00.000Z",
"updated_at": "2026-10-02T09:15:00.000Z"
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /products/{id}/publications/{pubId}
Take a listing down
Removes the listing at the portal and marks the publication deleted. Answers 200 with the tombstoned row, not 204 — the record survives so the history of where a product was listed does.
Remote removal is best-effort: if the publication never got an external_id, or the provider has no product seam, the local row is still marked deleted. Check the portal itself when it matters.
The \{id\} in the path is not used to locate the publication — pubId identifies it on its own, and the workspace check is against that row.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
pubId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/products/<id>/publications/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-000000000011",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"product_id": "7a7a7a7a-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC1234567890",
"status": "deleted",
"last_synced_at": "2026-10-02T09:15:00.000Z",
"last_published_at": "2026-10-02T09:15:00.000Z",
"last_error": null,
"raw_provider_payload": null,
"created_at": "2026-10-02T09:15:00.000Z",
"updated_at": "2026-10-04T11:30:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /products/{id}/publish
Publish to one or more portals
Pushes the canonical product to each of integration_ids, in order, and answers 200 with a per-portal outcome array — status is published or error for each, with the message on the failing ones. A 200 therefore does NOT mean everything published; read the array.
Re-publishing is how you push an edit: an existing publication supplies its external_id, so the portal listing is updated rather than duplicated.
Per-portal failures are recorded and kept going, but an integration_id the workspace does not own aborts the whole call with a 400 — and portals earlier in the array have already been published by then. That partial state is real and is not rolled back, so validate the ids before sending rather than after.
A portal whose provider has no product seam, or one with publishing disabled, comes back as a per-portal error. The product’s published_at is stamped on the first success and never cleared.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
integration_ids | uuid[] | yes |
curl -X POST https://api.vitrinadev.com/api/v1/products/<id>/publish \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"integration_ids": [
"33333333-0000-4000-8000-000000000001"
]
}'Example response (200)
{
"data": [
{
"integration_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"status": "published",
"external_id": "MLC1234567890",
"publication": {
"id": "7a7a7a7a-0000-4000-8000-000000000011",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"product_id": "7a7a7a7a-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC1234567890",
"status": "published",
"last_synced_at": "2026-10-02T09:15:00.000Z",
"last_published_at": "2026-10-02T09:15:00.000Z",
"last_error": null,
"raw_provider_payload": null,
"created_at": "2026-10-02T09:15:00.000Z",
"updated_at": "2026-10-02T09:15:00.000Z"
}
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /products/{id}/variants
Add a variant to a product
One purchasable variant (a talla × color combination) with its own options (ordered [\{name, value\}]), sku, price_amount (whole CLP; null = the product’s price) and available_quantity. The product’s available_quantity becomes the sum of its active variants’ stock. inventory_tracked: false or inventory_policy: "continue" keep a variant sellable at zero stock. attributes is a small flat map of per-variant facts (string → string | number | boolean; at most 20 keys, 2 KB), e.g. \{"battery_min_pct": 99, "battery_max_pct": 100\}; it reads \{\} when none.
Only a native product’s variants are edited here: a synced product answers 409. Answers 201 with the variant (available, label).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
sku | string | null | — | máx. 120 |
barcode | string | null | — | máx. 120 |
title | string | null | — | máx. 255 |
options | object[] | — | |
attributes | object | — | |
price_amount | number | null | — | ≥ 0 |
available_quantity | integer | — | ≥ 0 |
inventory_tracked | boolean | — | |
inventory_policy | deny \ | continue | — |
image_url | string | null | — | uri, máx. 2048 |
position | integer | — | ≥ 0 |
active | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/products/<id>/variants \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"sku": "POL-NEGRO-M",
"options": [
{
"name": "Color",
"value": "Negro"
},
{
"name": "Talla",
"value": "M"
}
],
"attributes": {
"battery_min_pct": 99,
"battery_max_pct": 100
},
"available_quantity": 4
}'Example response (201)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-000000000031",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"product_id": "7a7a7a7a-0000-4000-8000-000000000001",
"external_id": null,
"sku": "POL-NEGRO-M",
"barcode": null,
"title": null,
"options": [
{
"name": "Color",
"value": "Negro"
},
{
"name": "Talla",
"value": "M"
}
],
"option_tokens": [
"m",
"negr"
],
"attributes": {
"battery_min_pct": 99,
"battery_max_pct": 100
},
"price_clp": null,
"available_quantity": 4,
"inventory_tracked": true,
"inventory_policy": "deny",
"image_url": null,
"position": 0,
"active": true,
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-01T14:20:00.000Z",
"available": true,
"label": "Negro / M"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /products/{id}/variants/{variantId}
Delete a variant
A hard delete; the product’s stock is re-totalled from the remaining variants. To stop selling a variant but keep it, PATCH active: false instead. A synced product answers 409.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
variantId | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/products/<id>/variants/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /products/{id}/variants/{variantId}
Update a variant
Applies only the fields present. options and attributes are replaced wholesale (send the full map; \{\} clears it). Setting available_quantity updates the product’s rolled-up stock in the same write. A synced product answers 409; a variant of another product answers 404.
available_quantity WRITTEN here is the ON-HAND count (the units you have). Units held for unpaid storefront checkouts are separate rows a write never erases: the response (and every read) returns on_hand_quantity, held_quantity and available_quantity = on hand − held.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
variantId | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
sku | string | null | — | máx. 120 |
barcode | string | null | — | máx. 120 |
title | string | null | — | máx. 255 |
options | object[] | — | |
attributes | object | — | |
price_amount | number | null | — | ≥ 0 |
available_quantity | integer | — | ≥ 0 |
inventory_tracked | boolean | — | |
inventory_policy | deny \ | continue | — |
image_url | string | null | — | uri, máx. 2048 |
position | integer | — | ≥ 0 |
active | boolean | — |
curl -X PATCH https://api.vitrinadev.com/api/v1/products/<id>/variants/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"available_quantity": 2
}'Example response (200)
{
"data": {
"id": "7a7a7a7a-0000-4000-8000-000000000031",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"product_id": "7a7a7a7a-0000-4000-8000-000000000001",
"external_id": null,
"sku": "POL-NEGRO-M",
"barcode": null,
"title": null,
"options": [
{
"name": "Color",
"value": "Negro"
},
{
"name": "Talla",
"value": "M"
}
],
"option_tokens": [
"m",
"negr"
],
"attributes": {
"battery_min_pct": 99,
"battery_max_pct": 100
},
"price_clp": null,
"available_quantity": 2,
"inventory_tracked": true,
"inventory_policy": "deny",
"image_url": null,
"position": 0,
"active": true,
"created_at": "2026-10-01T14:20:00.000Z",
"updated_at": "2026-10-03T10:05:00.000Z",
"available": true,
"label": "Negro / M"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /products/publications
Publication state for many products at once
The batched read behind the inventory list: one request per page instead of one per row. ids is a comma-separated list of up to 100 uuids and the response is a flat array of publication rows across all of them — group by product_id client-side.
There are no per-id 404 semantics: an id in another workspace, or one that does not exist, simply contributes no rows. So a short answer means "these have no publications", not "one of your ids was wrong".
Registered before /\{id\} so publications is never read as a product id.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
ids | query | string | yes | mín. 1 |
curl https://api.vitrinadev.com/api/v1/products/publications \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "7a7a7a7a-0000-4000-8000-000000000011",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"product_id": "7a7a7a7a-0000-4000-8000-000000000001",
"marketplace_account_id": "33333333-0000-4000-8000-000000000001",
"provider": "mercadolibre",
"external_id": "MLC1234567890",
"status": "published",
"last_synced_at": "2026-10-02T09:15:00.000Z",
"last_published_at": "2026-10-02T09:15:00.000Z",
"last_error": null,
"raw_provider_payload": null,
"created_at": "2026-10-02T09:15:00.000Z",
"updated_at": "2026-10-02T09:15:00.000Z"
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
Price approvals
Authority over a below-reference price: the asker and the decider must be different people.
Projects
A real-estate developer's projects, their unit models, their stock of units (apartments, houses, parking spaces and storage units) and their versioned price list in UF and PDF quotes with down payment and mortgage payment, with an Excel import.