Ações em lote
Muda status, preço, categoria, seller ou campos de muitos produtos, ou clona e exclui, em uma chamada, com simulação e desfazer.
Esta é a rota de lote unificada. O campo action escolhe o que fazer, e você escolhe os produtos com uma lista de ids ou com um filtro. Use quando a mesma mudança deve alcançar muitos produtos: ocultar uma marca inteira, subir os preços em 10%, mover uma lista de produtos para outra categoria, excluir os descontinuados.
Leia antes a visão geral para o fluxo simular, confirmar e desfazer e para a lista de chaves do filtro. O produto desta seção é identificado pelo UUID; o dinheiro é em centavos inteiros.
/api/v1/admin/products/bulkAdminAção em lote sobre produtos
Exige o privilégio manage_catalog. O corpo é lido até 16 MiB. Não há número máximo fixo de ids, mas prefira um filter para mais do que alguns milhares de produtos.
action | O que faz | Campos extras | Desfazível |
|---|---|---|---|
update_status | Define o status de todos os produtos | status | Sim |
update_category | Move produtos para outra categoria (ela vira a categoria principal) | category_id | Sim |
update_price | Muda o preço de venda | price_action, price_value | Sim |
delete | Manda produtos para a lixeira | nenhum | Sim (restaura) |
clone | Cria uma cópia de cada produto | nenhum | Não (exclua as cópias) |
update_vendor | Atribui os produtos a outro seller | vendor_id | Não |
update_field | Edita campos por transformação (acrescentar texto, trocar, somar um valor...) | changes | Sim |
Ações de preço em price_action (os nomes antigos absolute, increase, decrease, percentage_increase e percentage_decrease também são aceitos):
price_action | price_value | Resultado |
|---|---|---|
set (ou set_fixed) | centavos | Define o preço neste valor (maior que zero) |
increase_fixed | centavos | Soma este valor |
decrease_fixed | centavos | Subtrai este valor, nunca abaixo de zero |
increase_percent | pontos-base, de 0 a 10000 | Soma este percentual (500 = +5%) |
decrease_percent | pontos-base, de 0 a 10000 | Subtrai este percentual |
price_value é obrigatório e não pode ser zero. Uma mudança que deixaria algum produto com preço zero é recusada para o lote inteiro.
update_field edita os campos name, sku, ean, brand, price_cents, list_price_cents, quantity, weight, length, width, height, status, seo_title e seo_description. Cada item de changes é {"field", "op", "value", "find"}:
| Tipo do campo | op permitidas |
|---|---|
Texto (name, sku, ean, brand, seo_title, seo_description) | set, append, prepend, replace (exige find), clear |
Número (price_cents e list_price_cents em centavos, quantity, weight, length, width, height) | set, increase, decrease |
status | set |
update_field sempre passa pelo fluxo de simular e confirmar, mesmo com lista de ids. Os campos de produtos ligados a um produto mestre seguem as regras de campos editáveis do mestre, e as linhas que não podem mudar vêm em failed.
Corpo da requisição
actionstringobrigatórioupdate_statusupdate_categoryupdate_pricedeletecloneupdate_vendorupdate_fieldproduct_idsarrayobrigatóriofilter. Cada id precisa ser um UUID.filterobjectproduct_ids é ignorado. Exige o fluxo de simular e confirmar.simulatebooleanpadrão: falseconfirm_tokenstringfilter.statusstringADHactivedisabledhiddeninactiveupdate_status. R e X são recusados aqui.category_iduuidupdate_category. Precisa existir e não estar na lixeira.vendor_iduuidupdate_vendor. Precisa ser um seller ativo.price_actionstringsetset_fixedincrease_fixeddecrease_fixedincrease_percentdecrease_percentupdate_price.price_valueintegerupdate_price: centavos, ou pontos-base nas ações de percentual.changesarrayupdate_field, uma entrada por campo.Exemplo de requisição
Simule um aumento de 10% em todos os produtos ativos de uma marca e depois execute com o token.
# 1) simular
curl -X POST "$BASE_URL/api/v1/admin/products/bulk" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"action": "update_price",
"price_action": "increase_percent",
"price_value": 1000,
"filter": { "brand": "acme", "status": "active" },
"simulate": true
}'
# 2) executar: mesmo corpo, sem "simulate", com o confirm_token do passo 1
curl -X POST "$BASE_URL/api/v1/admin/products/bulk" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"action": "update_price",
"price_action": "increase_percent",
"price_value": 1000,
"filter": { "brand": "acme", "status": "active" },
"confirm_token": "eyJ0IjoiZGZl..."
}'Resposta
{
"simulate": true,
"action": "update_price",
"description": "Aumentar o preço dos produtos em 10%",
"total": 2,
"sample": [
{ "id": "a34f126e-ff3b-4779-bf74-bda03502136e", "name": "Caneca Azul", "before": "R$ 49,90", "after": "R$ 54,89" },
{ "id": "ea4e5a3d-bed2-4bcc-bce4-53a8370cee28", "name": "Camiseta Azul", "before": "R$ 49,90", "after": "R$ 54,89" }
],
"confirm_token": "eyJ0IjoiZGZl...",
"expires_at": "2026-10-11T22:04:00Z",
"async": false,
"by": "filter"
}{
"action": "update_price",
"by": "filter",
"total_requested": 2,
"status": "done",
"affected": 2,
"failed": [],
"undo_id": "ad40a3ab-042f-4495-bb33-c44cb51cfdff"
}{
"action": "update_price",
"by": "filter",
"total_requested": 5000,
"status": "running",
"async": true,
"job": {
"id": "7c1b6f0e-2d0e-4c6e-9d3a-0d6c1f6a9b11",
"status": "running",
"total": 5000,
"processed": 0,
"affected": 0,
"failed": 0,
"percent": 0
}
}{ "action": "update_status", "affected": 2, "undo_id": "5b1d9f9c-4f55-4a70-bd2d-2d6c7e5d2f10" }{
"action": "update_status",
"simulate": true,
"total_affected": 0,
"preview": [
{ "product_id": "ea4e5a3d-bed2-4bcc-bce4-53a8370cee28", "name": "Camiseta Azul", "current_value": "A", "new_value": "H", "ok": true },
{ "product_id": "00000000-0000-0000-0000-000000000000", "ok": false, "reason": "not_found" }
]
}{ "action": "clone", "cloned": 1, "products": [{ "id": "81f223bc-9dec-4a0a-9c40-4263ac893973", "name": "Camiseta Azul (Copy)", "status": "R" }] }{ "action": "update_vendor", "affected": 3 }Campos da resposta
simulatebooleantrue nas simulações.totalintegersamplearrayid, name, before e after. Os preços da amostra vêm formatados para leitura (R$ 54,89).confirm_tokenstringexpires_atstringasyncbooleantrue quando a execução criará uma tarefa em segundo plano (50 registros ou mais).notestringpreviewarrayproduct_id, name, current_value, new_value (textos; preço em centavos, categoria como UUID), ok, e reason (not_found) quando ok é falso.total_affectedinteger0: nada foi gravado.total_requestedintegerstatusstringdone, ou queued/running quando assíncrono.affectedintegerfailedarray{id, error}.partial_failurebooleantrue quando alguns produtos falharam. Uma message diz quantos.undo_idstringPOST /api/v1/admin/bulk-undo/{undo_id}. Ausente em clone e update_vendor. Em tarefa em segundo plano fica em job.result.jobobjectid, status, total, processed, affected, failed, percent, errors, result, created_at, started_at, finished_at.clonedintegerclone) Número de cópias. products as lista. A cópia não tem SKU e nasce com status R (aguardando revisão), então não aparece na loja.Erros
| Status | Quando | Exemplo de corpo |
|---|---|---|
| 400 | Corpo não é JSON válido | {"error":"invalid request body","detail":"invalid character 'b' looking for beginning of object key string"} |
| 400 | Sem ids e sem filtro | {"error":"product_ids is required and must not be empty"} |
| 400 | Ação desconhecida | {"error":"invalid action","detail":"action must be one of: update_status, delete, clone, update_category, update_price, update_vendor"} |
| 400 | Um id não é UUID | {"error":"invalid product_ids","detail":"... \"abc\" não é um uuid de produto ..."} |
| 400 | Status não aceito em lote (R, X, desconhecido) | {"error":"invalid product status","detail":"invalid product status: R (must be A/D/H or active/hidden/disabled/inactive/pending)"} |
| 400 | price_action desconhecida | {"error":"invalid price_action","detail":"must be one of: set, increase_fixed, decrease_fixed, increase_percent, decrease_percent"} |
| 400 | price_value ausente ou zero | {"error":"price_value is required for update_price action","detail":"..."} ou {"error":"price_value cannot be zero",...} |
| 400 | Preço fora da faixa, ou a mudança deixaria um produto em zero | {"error":"invalid bulk price update","detail":"percentage must be between 0 and 10000 (basis points)"} |
| 400 | Chave de filtro desconhecida, filtro vazio, changes inválido | {"error":"invalid_filter","detail":"filtro de lote inválido: chave \"colour\" não existe ... Chaves aceitas: ..."} |
| 400 | category_id não é UUID | {"error":"invalid bulk category move","detail":"..."} |
| 401 | Sem token ou token inválido | {"error":"missing authorization header"} |
| 403 | Sem manage_catalog | {"error":"insufficient privileges","required":"manage_catalog"} |
| 404 | Categoria de destino não existe ou está na lixeira | {"error":"category not found","detail":"nenhuma categoria ativa com o id ..."} |
| 409 | Token de outro corpo, expirado, ou o filtro agora alcança mais registros | {"error":"confirm_token_invalid","detail":"confirm_token não vale para este pedido: ..."} |
| 428 | Lote por filtro sem confirm_token | {"error":"simulate_required","detail":"lote por filtro exige simulação: ..."} |
| 500 | update_vendor com seller inexistente ou inativo, ou qualquer falha do servidor | {"error":"failed to bulk update vendor","detail":"vendor not found or not active"} |
Os textos de detail são escritos em português pelo servidor e voltam como estão.