Lote genérico
Aplica a rota de edição de UM registro a muitos registros de uma vez, com simulação obrigatória, confirmação, tarefa em segundo plano e desfazer.
O lote genérico pega qualquer rota de escrita de um registro do painel (por exemplo PUT /products/{id} ou PUT /promotions/{id}) e a repete para todos os registros de um recorte. Para cada registro, a plataforma lê o detalhe, aplica as mudanças e envia o resultado à rota de edição de sempre, com o seu token. Por isso as validações, as permissões e a auditoria são as da edição manual: se você não pode editar um registro, o lote falha naquele registro.
O fluxo é sempre em dois tempos. Primeiro você simula (simulate: true), que não grava nada e devolve o total, uma amostra antes e depois e um confirm_token. Depois você executa o mesmo corpo com o token. Veja o resumo do fluxo.
Aplicar um lote
/api/v1/admin/bulk/applyAdminSimular ou executar uma rota de um registro sobre muitos
Exige o privilégio manage_catalog e o privilégio da rota interna escolhida. O token de acesso (Authorization: Bearer) é obrigatório e é o que a plataforma usa em cada chamada interna.
Corpo da requisição
routestringobrigatórioA rota de escrita de um registro, no formato "VERBO /caminho/{id}", onde VERBO é o método HTTP, relativa a /api/v1/admin. Exemplos: PUT /products/{id}, PUT /promotions/{id}, DELETE /banners/{id}. Métodos aceitos: POST, PUT, PATCH, DELETE. Rotas sem {id} só valem para POST (que cria um registro por linha). Não podem ir em lote: credenciais e chaves de API, "entrar como" outro usuário, rotas /root/ e o próprio lote.
filterobjectO recorte, por critério. As chaves dependem do recurso (para produtos, por exemplo category_id, price_max, out_of_stock; veja a tabela abaixo). {"ids": ["..."]} vale para qualquer recurso. {"all": true} pega a tabela inteira e deve ser usado só quando é isso mesmo que se quer. Uma chave desconhecida volta 400 com a lista das chaves aceitas. Um filtro sem nenhuma chave é recusado.
liststringquery.queryobjectlist, por exemplo {"status":"D"}.whereobject{"priority":{"$gte":5}}. Operadores: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $exists, $empty.where_detailobjectwhere, mas sobre o detalhe de cada registro, lido um a um (mais lento).allbooleanlist, percorre a listagem inteira, sem critério.rowsarrayid e suas próprias changes ou body.detailstring/api/v1/admin) do GET que lê cada registro antes da alteração. Só é preciso quando a plataforma não acha sozinha a rota de detalhe certa.parentobjectPara listagens de subrecurso com marcador, como list: "/products/{parent.id}/variations": o recorte dos registros pai, com filter, list, query, where ou all. A listagem roda uma vez para cada pai e a rota pode usar {parent.id}. Obrigatório quando list tem marcador.
changesarrayO que mudar em cada registro, descrito sobre o JSON que a rota de detalhe devolve. Use changes ou body, não os dois.
pathstringobrigatórioprice, bonuses ou condition_groups.conditions. * é qualquer filho e ** o nó e todos os descendentes.opstringobrigatóriosetclearunsetincreasedecreasemultiplyincrease_percentdecrease_percentappendprependreplaceshifttoggleaddadd_allremoveupdatecomputeA operação. set define o valor; clear esvazia; unset remove o campo; increase, decrease e multiply fazem conta; increase_percent e decrease_percent aplicam porcentagem; append e prepend acrescentam texto; replace troca o trecho find por value; shift desloca datas ("7d", "-12h", "2w", "1mo", "1y"); toggle inverte um booleano; add, add_all e remove mexem em listas; update mescla value nos objetos que casam com match; compute calcula um valor.
valueany"{{name|upper}}" com os filtros upper, lower, short, trim.matchobject{"type":"subtotal"}).findstringreplace.whenobjectbodyobjectPOST, o corpo de cada registro criado, com modelos {{campo}}). Alternativa a changes.descriptionstringsimulatebooleanpadrão: falsetrue apenas simula.confirm_tokenstringChaves de filtro de produtos
Para PUT /products/{id} e as demais rotas de produto. Dinheiro sempre em centavos.
| Chave | Significado |
|---|---|
name_contains, name_starts_with | Parte do nome ou começo do nome. |
sku, sku_contains, ean | SKU exato (ou lista), parte do SKU, EAN exato (ou lista). |
brand | Marca, sem diferenciar maiúsculas. |
status | active (A), disabled (D), hidden (H), pending (R) ou rejected (X); aceita a letra ou a palavra. |
category_id, category_tree | Categoria exata (UUID ou lista); categoria e todas as subcategorias. |
vendor_id, without_vendor | Seller (UUID ou lista); true para só produtos da própria loja. |
price_min, price_max | Faixa de preço, em centavos. |
list_price_min, list_price_max, cost_min, cost_max | Faixas de preço de lista e de custo, em centavos. |
quantity_min, quantity_max, out_of_stock | Estoque; out_of_stock: true para sem estoque. |
has_image, has_description, has_ean | false pega os que não têm. |
is_variation, free_shipping | Variações ou principais; frete grátis. |
sales_min, sales_max | Vendas; sales_max: 0 pega os que nunca venderam. |
updated_before, created_after, created_before | Datas AAAA-MM-DD. |
ids, exclude_ids, all | Lista de ids; ids a deixar de fora; a tabela inteira. |
Os recursos com filtro por critério são: produtos, categorias, características, produtos mestres, pedidos, usuários, documentos de seller, banners, avaliações, promoções, cupons, mudanças de preço propostas, fila fiscal, conversas e mídias de WhatsApp, preferências de notificação, redirecionamentos de SEO, regras de CNAE, lançamentos financeiros e versões. Para os outros recursos use list e query, rows ou ids.
Exemplo de simulação
curl -X POST "$BASE_URL/api/v1/admin/bulk/apply" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"route": "PUT /products/{id}",
"filter": {"category_id": "ae6f8f8b-7cbe-4725-84fa-247cce138143", "price_max": 5000},
"changes": [{"path": "price", "op": "increase_percent", "value": 10}],
"simulate": true
}'Resposta da simulação
{
"simulate": true,
"action": "PUT /products/{id}",
"description": "Editar produtos: aumentar price em 10%",
"route": "PUT /products/{id}",
"route_risk": "safe",
"by": "route",
"total": 1,
"will_change": 1,
"skipped": 0,
"failures": 0,
"fields": ["price"],
"undoable": true,
"async": false,
"note": "1 mudam, 0 já estão assim e serão pulados, 0 não aceitam o pedido",
"sample": [
{
"id": "d8a6c5e7-8e44-4055-a0e2-49014f5d17f6",
"name": "Notebook Vintage Vermelho",
"before": "price: R$ 1.678,95",
"after": "price: R$ 1.846,85"
}
],
"confirm_token": "eyJ0Ijoi...",
"expires_at": "2026-10-11T22:36:31Z"
}Campos da resposta
simulatebooleantrue.actionstringdescriptionstringroute_riskstringsafesensitivedestructivedestructive merece confirmação humana redobrada.totalintegerwill_changeintegerskippedintegerfailuresintegerfailure_samplesarrayid e error).fieldsarrayoverwritesintegerwarningstringundoablebooleanasyncbooleansamplearrayid, name, before, after.confirm_tokenstringexpires_atstringResposta da execução
{
"action": "PUT /products/{id}",
"by": "filter",
"total_requested": 3,
"status": "done",
"affected": 3,
"failed": [],
"undo_id": "0e6c0167-9b7b-4f72-b810-7c0768d4ce8e",
"skipped": 0
}{
"action": "PUT /products/{id}",
"by": "filter",
"total_requested": 129,
"status": "queued",
"async": true,
"job": {
"id": "7341b071-4e43-46fc-9470-19a04cb8d048",
"route": "POST /bulk/apply",
"action": "PUT /products/{id}",
"description": "Editar produtos: calcular price = round(price * 0.9)",
"status": "queued",
"total": 129,
"processed": 0,
"affected": 0,
"failed": 0,
"percent": 0,
"created_at": "2026-09-15T21:52:29Z"
}
}Campos da resposta
total_requestedintegerstatusstringqueuedrunningdonefailedinterruptedaffectedintegerfailedarrayid e error. Só na execução imediata.partial_failurebooleantrue quando algum registro falhou. Ausente quando tudo passou.messagestringundo_iduuidskippedintegerasyncbooleantrue na resposta 202.jobobjectErros
| Status | Quando | Corpo de exemplo |
|---|---|---|
| 400 | route malformada, método GET, rota inexistente no painel ou proibida em lote. | {"error":"invalid_filter","detail":"filtro de lote inválido: a rota POST /impersonate não existe no painel; ..."} |
| 400 | filter com chave desconhecida, ou valor fora do domínio (o detail lista as chaves aceitas). | {"error":"invalid_filter","detail":"filtro de lote inválido: chave \"foo\" não existe no filtro de produtos. Chaves aceitas: ..."} |
| 400 | Sem changes nem body, ou os dois ao mesmo tempo. | {"error":"invalid_filter","detail":"filtro de lote inválido: mande changes (...) OU body (...), não os dois"} |
| 401 | Sem token Bearer. | {"error":"unauthorized","detail":"..."} |
| 403 | Sem manage_catalog. | {"error":"insufficient privileges","required":"manage_catalog"} |
| 409 | Token de outro pedido, de outro usuário, expirado, ou o recorte cresceu desde a simulação. | {"error":"confirm_token_invalid","detail":"confirm_token não vale para este pedido"} |
| 428 | Execução sem confirm_token. | {"error":"simulate_required","detail":"lote por filtro exige simulação: ..."} |
| 500 | Falha interna do lote. | {"error":"bulk operation failed","detail":"..."} |
Os textos em detail desta área chegam em português.