Leituras e entrega
A leitura é um recurso: você lista o que já rodou ("a das 10h"), acompanha o que está rodando e drena os itens com cursor retomável. Os itens vêm no vocabulário schema.org Product+Offer - o mesmo que os sites publicam, sem manual pra decorar.
As rotas
GET /v1/reads as leituras (filtros: store, channel, período, status, item_list)
GET /v1/reads/{read_id} o resumo de uma leitura
GET /v1/reads/{read_id}/items os itens dela - é aqui que você consome
GET /v1/{store}/products/{id} consulta pontual - {id} = catálogo, sku OU o seu alias
GET /v1/{store}/products/{id}/history série temporal (90 dias)
{store} aceita o slug (mercadolivre) ou o id
numérico que você já usa (468): número é sempre store_id, texto é
sempre slug. Loja que não atendemos responde 404; item sem oferta não
é 404, é resposta (NO_OFFERS).
{id} segue a mesma ideia: aceita o id de catálogo
(MLB16039858), o sku ou o seu alias
(MLB25708216-DIVINA_FARMA-PRICE-F-1) - você nunca precisa fazer o de/para
ao contrário para perguntar por um item. A resposta volta na chave que você pediu.
O item_list também aceita um alias
(MLB22558754-ABCDACONSTRUCAO-PRICE-F-1) ou só a cauda dele: a gente lê a
lista de dentro. O alias parece conter a lista, mas
ABCDACONSTRUCAO-PRICE-F-1 e ABCDACONSTRUCAO-F não são a mesma
string - colar as duas pontas é trabalho nosso.
"Sem oferta" tem dois motivos diferentes:
monitored, not read yet é item seu, na sua lista, que ainda não
teve leitura própria - é só esperar. item not monitored é referência que
nenhuma linha sua nomeia - aí é corrigir a lista. Enquanto uma carteira nova entra, o
primeiro é a maioria: a identidade de um item se lê uma vez.
Filtrar por lista: GET /v1/reads?item_list=KABUM-F
devolve só as leituras daquela lista, com o nome que você deu a ela. Não
diferencia maiúscula de minúscula, e lista que não existe devolve vazio - nunca a
carteira inteira. Combina com os outros filtros
(?item_list=KABUM-F&status=DONE).
Qual leitura é esta
Uma lista pode ser lida várias vezes ao dia - a KABUM-F
é lida às 00:30, 06:30 e 11:30 - e as três respondem o mesmo
item_list. O que as separa é scan (1, 2, 3…
dentro da sua lista) e scan_time, o horário em que ela é
configurada.
Não use scheduled_for para isso. Ele é o instante em
que a leitura de fato rodou: numa re-execução, as leituras do dia de uma mesma lista
disparam com segundos de diferença, e a atribuição por horário erraria.
O seu código de volta
product.external_id é o seu alias, devolvido
exatamente como chegou na sua lista
(MLB25708216-DIVINA_FARMA-PRICE-F-1). product.id é o id de
catálogo da loja - nosso, de descobrir. Sem o primeiro, você manteria à mão um de/para
de algo que a gente já sabe.
Ele é por lista: o mesmo item pode ser monitorado por dois clientes
finais seus, com dois códigos, e cada leitura devolve o daquela lista - inclusive
quando as duas apontam para a mesma página, que a gente lê uma vez e entrega duas.
external_id vem null só quando não temos nenhuma linha sua
daquele item, tipicamente numa leitura de vitrine, que varre a loja inteira e traz
itens que você nunca nos mandou. Nunca o código do cliente ao lado, porque código
errado reconcilia em silêncio contra o item errado.
Sem oferta não é uma coisa só
NO_OFFERS vem com no_offers_reason, porque o campo
misturava fatos diferentes:
OUT_OF_STOCK- a página de catálogo está viva e a lista de vendedores voltou vazia: ninguém está ofertando agora. É a ruptura, e é a maioria dos casos.NO_BUYBOX- a página é de vendedor único por construção (/up/MLBU…), então não há lista de vendedores para estar vazia. Não diz nada sobre estoque.STANDALONE- não encontramos página de catálogo para o item, então nunca houve lista de vendedores para ler. O item existe e é avulso.LISTING_GONE- o anúncio saiu do ar: a página do item não existe mais. Não é sobre estoque nem sobre catálogo - a referência da sua lista precisa ser trocada para o item voltar a ser monitorado. O Mercado Livre não devolve 404 em anúncio morto: redireciona para uma busca pelo nome do produto, e até agosto/2026 isso era entregue comoSTANDALONE.
É um campo, não um quarto status: quem já trata NO_OFFERS
continua funcionando sem mexer em nada.
Item sem oferta volta com o veredito daquela leitura e offers vazio -
a entrega diz o que esta leitura viu, e não carrega preço de leitura anterior
(o campo last_known_price saiu em 31/08/2026). Preço já coletado você
consulta onde ele mora: GET /v1/{store}/products/{id} e
.../history.
Contexto na raiz: uma leitura = um contexto
site é uma leitura, app é outra, app + RS é outra.
O contexto qualifica a leitura inteira e vem na raiz de todo envelope - nunca repetido item a
item, sempre presente, com null explícito no que não se aplica.
{
"channel": "app", // site | app | in_store
"region": {
"country": "BR", "state": null, "city": null,
"postal_code": "93265001", "lat": null, "lng": null,
"branch_id": "3078" // filial, quando a leitura é de loja física
}
}
O resumo de uma leitura
{
"read_id": 4821,
"store": "mercadolivre", "store_id": 468,
"context": { ... },
"scheduled_for": "2026-08-05T10:00:00-03:00", // o slot ("a das 10h")
"started_at": "2026-08-05T10:00:12-03:00",
"finished_at": null, // null enquanto roda
"status": "RUNNING", // RUNNING | DONE | PARTIAL
"items": { "expected": 52000, "ok": 47570, "no_offers": 4210, "no_results": 0, "failed": 110 }
}
Os contadores batem 1:1 com os status dos itens em /items. PARTIAL
= terminou com item FAILED (falha nossa, nunca cobrada).
Os itens: cursor retomável
Ordem sempre por item_id crescente (ordem de landing). after é o
último item_id que você recebeu: parou na página 10, recomeça de onde parou, sem
reprocessar e sem pular. Idempotente. Com a leitura RUNNING, item novo entra no fim
(id maior) - dá pra consumir enquanto roda.
{
"item_id": 2049,
"status": "OK", // OK | NO_OFFERS | NO_RESULTS | FAILED
"collected_at": "2026-08-05T10:00:00-03:00",
"product": {
"id": "MLB1234567890", "name": "Torneira Docol Lift 00872006",
"brand": "Docol", "gtin": "7891461329723", "sku": "00872006",
"url": "https://...", "image": "https://...",
"category": ["Casa", "Banheiros", "Torneiras"]
},
"offer_count": 2, // ofertas LIDAS, nunca o que o site anuncia
"offers": [{
"rank": 1, // ordem que a loja serviu
"is_featured": true, // ganhou a buybox / oferta em destaque
"price": 199.90, "list_price": 249.90, "price_currency": "BRL",
"availability": "InStock",
"seller": { "id": "1234567", "name": "Loja Y", "is_official": false },
"installments": { "count": 12, "value": 18.90 },
"payment_prices": { "pix": 189.90 }
}]
}
offers é sempre lista, mesmo com uma oferta só: loja com buybox e loja sem
buybox têm a mesma forma - um parser só. rank 1 é a ordem da loja, que nem sempre
é o mais barato: não reordenamos por preço.
Leitura de listagem: a posição na relação
Uma listagem - termo de busca, categoria ou departamento - é uma leitura e
sai pelas mesmas rotas: mesmo envelope, mesmo cursor, contexto na raiz. Uma leitura = uma
fonte: o termo tenis nike das 9h e a categoria Calçados
das 9h são duas leituras, cada uma com seu read_id. Elas aparecem em
GET /v1/reads junto com as demais, e quem só quer listagem filtra pelas que trazem
listing preenchido.
{
"read_id": 5104,
"store": "mercadolivre", "store_id": 468,
"context": { ... }, // na raiz: a posição é sempre a DESTE contexto
"status": "DONE",
"listing": {
"source": { "id": 7, "type": "SEARCH", // SEARCH = termo | SECTOR = categoria/departamento
"name": "tenis nike",
"url": "https://lista.mercadolivre.com.br/tenis-nike" },
"positions": 63, // posições entregues por esta leitura
"last_rank": 63, // a posição em que a relação terminou
"complete": true // a relação foi varrida até o fim
},
"totals": { "expected": 63, "ok": 63, // a LEITURA inteira -- mesmo bloco que /reads traz como "items"
"no_offers": 0, "no_results": 0, "failed": 0 },
"count": 63, // itens NESTA página (não o total: esse é totals.expected)
"next_after": null,
"items": [{
"item_id": 90114,
"rank": 1, // a posição na relação exibida
"status": "OK",
"collected_at": "2026-08-05T09:01:00-03:00",
"product": { "id": "MLB101", "url": "https://...", "name": "Tenis Nike Revolution" },
"offer_count": 0, "offers": []
}]
}
A ordem do cursor é a ordem da relação. A varredura entrega a página 1 antes
da 2 e a posição 1 antes da 2, então drenar por ?after= devolve o ranking na ordem;
rank diz a posição explicitamente em vez de deixá-la implícita.
Posição não é oferta. A listagem lê a relação, não o preço de cada item: os
itens vêm com offers: []. Para preço, use a leitura de item
(/v1/{store}/products/{id}). A identidade garantida do item é id +
url; name vem preenchido quando já conhecemos o item.
A posição só significa algo com o contexto. Marketplace personaliza e
regionaliza ranking: posição 3 é sempre posição 3 no contexto declarado, nunca absoluta.
Por isso o context está na raiz da resposta, e não por item.
complete: false nunca é passado por fim de relação. Enquanto a
leitura roda, e quando uma página dela não pôde ser lida, não afirmamos que a relação acabou:
last_rank diz o que foi entregue, e nada além disso.
NO_RESULTS é resposta, não falha. Relação lida e vazia vem como
um item de status NO_RESULTS, sem product e sem rank - e é
cobrada, como qualquer resposta. Falha de coleta continua FAILED e nunca é cobrada.
Re-ler não duplica. Rodar a mesma leitura de novo move a posição do item no
lugar; o item_id dele (o cursor) é preservado.
Consulta pontual e histórico
A leitura mais recente de um item, no mesmo formato dos itens acima (envelope com
request_id, context na raiz e results). query
devolve exatamente o que você mandou - reconciliação sem depender de ordem.
Uma entrada por leitura passada (instante + ofertas), mais nova primeiro, janela de 90 dias.
Cobrança
O faturável nasce na coleta, não aqui: consultar esta API - paginação,
retry, reprocessamento - nunca cobra a mais. FAILED (falha nossa) nunca é
cobrado. Detalhes em Uso e faturamento.
Webhook (previsto)
Aviso por leitura (nunca por item): um POST com
{ "event": "read.finished", "read": { ...o resumo... } } quando a leitura fechar.
Ainda não implementado; até lá, GET /v1/reads?status=RUNNING cobre o caso.