Upsert por Lote
Crie ou atualize até 500 comércios em uma única operação, com seus endereços e usuários, e relatório de resultado por item.
POST /api/integration/commerces/batch-upsert
Cria ou atualiza (upsert) até 500 comércios em uma única operação, incluindo seus endereços e usuários. É o ponto de entrada de integração para carregar o catálogo de comércios a partir de um sistema externo (ERP, CRM).
Auth: Obrigatório — permissão commerce:batch-upsert
Sucesso parcial. Depois que o corpo passa na validação de formato, cada
item é processado de forma independente: os que podem ser persistidos são
persistidos, e os que falham são reportados em errors[] com um código
estável — um item que falha não desfaz os demais. Sempre verifique
summary.failed e errors na resposta.
Chave do upsert
Cada item é procurado entre os comércios existentes por external_id. Se existir um comércio com esse external_id, ele é atualizado (conta em summary.updated); caso contrário, é criado (summary.created). Na atualização, metadata é mesclada com a existente: as chaves enviadas sobrescrevem as de mesmo nome, as não enviadas são mantidas, e enviar uma chave com null a remove. A mesclagem é recursiva (RFC 7386 JSON Merge Patch): os objetos aninhados são mesclados nível por nível, então você pode atualizar uma chave dentro de um objeto aninhado sem reenviar as irmãs. Qualquer outro valor substitui o anterior por completo, incluindo os arrays, que nunca são mesclados elemento a elemento. Enviar metadata: null esvazia o saco inteiro.
O tax_id não pode ser compartilhado entre comércios do tenant. Se o tax_id enviado pertence a outro comércio que já existia antes do lote, ou a um item anterior do mesmo lote que não falhou, o item inteiro vai para errors e nada dele é gravado — reenviar a um comércio o seu próprio tax_id não é conflito. Liberar um tax_id em um item não o libera para um item posterior da mesma requisição: ele fica disponível apenas na seguinte. Enviar tax_id: "" equivale a null e apaga o valor gravado, enquanto omitir o campo o mantém.
Correspondência de endereços
Um endereço não é identificado pela sua posição no array. Cada elemento de addresses é procurado entre os endereços que o comércio já tem, e só é criado quando não corresponde a nenhum.
- Com
external_id— a correspondência é exata: o endereço desse comércio que carrega o mesmoexternal_id. Se nenhum o carrega, um endereço novo é criado e a heurística abaixo não é tentada. - Sem
external_id(ausente,nullou"") — duas heurísticas são tentadas em ordem: primeiro um endereço com o mesmoformatted_address, só se o item enviarformatted_address; depois um endereço com os mesmosstreet,street_numberecommune, só se o item enviar os três.
Quando nenhum corresponde, o endereço é criado. Enviar external_id em cada endereço é a forma de manter uma identidade estável entre cargas em vez de depender da heurística.
Linhas principais
is_primary está descontinuado e é ignorado. Ele continua sendo aceito em
endereços e contatos para não quebrar os callers que já o enviam, mas o valor
não tem efeito: a plataforma decide qual linha é a principal.
Um endereço ou um contato que já existe nunca muda seu is_primary por este endpoint. Uma linha nova nasce principal apenas quando seu grupo ainda não tem nenhuma linha ativa, e apenas a primeira linha nova desse grupo dentro do item — o grupo é o comércio para os endereços, e o par usuário + contact_type para os contatos. "Primeira" segue a ordem do array no payload.
Linhas da plataforma
As linhas com source: RETEN foram inseridas a partir da plataforma da Reten, e uma carga em massa não tem autoridade sobre elas. Um endereço que corresponde a um RETEN mantém todas as suas colunas sem alteração, e também não é criado um duplicado para ele. Em um usuário com source: RETEN o name não é tocado — suas funções e seus contatos são mesclados normalmente. Todo endereço, usuário e contato que este endpoint cria é gravado com source: INTEGRATION e é atualizado normalmente nas cargas seguintes.
Corpo da Requisição
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
items | object[] | Sim | Lista de comércios a gravar (mínimo 1, máximo 500). |
Cada elemento de items:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
external_id | string | Sim | Identificador do comércio no sistema externo. Chave do upsert. |
name | string | Sim | Nome do comércio. |
global_external_id | string | Não | Identificador externo adicional do comércio. Não é usado para a correspondência. |
tax_id | string | Não | Identificador tributário. Não pode estar em uso por outro comércio do tenant. "" equivale a null e o apaga. |
metadata | object | Não | JSON arbitrário. Mesclado recursivamente com a metadata existente na atualização. |
addresses | object[] | Não | Endereços do comércio (máximo 20, veja abaixo). |
users | object[] | Não | Usuários do comércio (máximo 20, veja abaixo). |
Cada elemento de addresses (todos os campos são opcionais):
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
external_id | string | Não | Identificador do endereço no sistema externo. Chave de correspondência — veja Correspondência de endereços. |
address_alias | string | Não | Rótulo do endereço (p. ex. "Loja matriz"). |
is_primary | boolean | Não | Descontinuado — aceito, porém ignorado. |
country_code | string | Não | Código do país. |
region | string | Não | Região / estado. |
commune | string | Não | Comuna / município. |
city | string | Não | Cidade. |
street | string | Não | Nome da rua. |
street_number | string | Não | Número. |
unit | string | Não | Unidade, andar ou sala. |
formatted_address | string | Não | Endereço completo em uma única string. |
latitude | number | Não | Latitude, entre -90 e 90 inclusive. Fora do intervalo manda o item inteiro para errors. |
longitude | number | Não | Longitude, entre -180 e 180 inclusive. Fora do intervalo manda o item inteiro para errors. |
Cada elemento de users:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
external_id | string | Sim | Identificador do usuário no sistema externo. |
name | string | Não | Nome do usuário. |
roles | enum[] | Não | Funções do usuário no comércio (máximo 10): OWNER, ORDERS, FINANCE, ATTENTION, ADMIN, COMMERCE_REP, OTHER. |
contacts | object[] | Não | Contatos do usuário (máximo 20, veja abaixo). |
Cada elemento de users[].contacts:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
contact_type | enum | Sim | Tipo de contato: PHONE, EMAIL, WHATSAPP ou OTHER. |
value | string | Sim | Valor do contato. Apenas EMAIL tem o formato validado; PHONE, WHATSAPP e OTHER são gravados como enviados. |
is_primary | boolean | Não | Descontinuado — aceito, porém ignorado. |
Exemplo
curl -X POST "https://api.reten.ai/api/integration/commerces/batch-upsert" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"external_id": "store-001",
"name": "Main Street Store",
"tax_id": "76123456-7",
"metadata": { "segment": "gold" },
"addresses": [
{
"external_id": "addr-001",
"address_alias": "Main store",
"country_code": "CL",
"region": "Región Metropolitana",
"commune": "Providencia",
"city": "Santiago",
"street": "Av. Providencia",
"street_number": "1234",
"latitude": -33.4489,
"longitude": -70.6693
}
],
"users": [
{
"external_id": "user-001",
"name": "María García",
"roles": ["OWNER"],
"contacts": [
{ "contact_type": "PHONE", "value": "+56912345678" },
{ "contact_type": "EMAIL", "value": "maria@example.com" }
]
}
]
},
{
"external_id": "store-002",
"name": "Downtown Store"
},
{
"external_id": "store-003",
"name": "Harbor Store",
"tax_id": "76123456-7"
}
]
}'const response = await fetch(
"https://api.reten.ai/api/integration/commerces/batch-upsert",
{
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [
{
external_id: "store-001",
name: "Main Street Store",
tax_id: "76123456-7",
metadata: { segment: "gold" },
addresses: [
{ external_id: "addr-001", address_alias: "Main store", country_code: "CL", region: "Región Metropolitana", commune: "Providencia", city: "Santiago", street: "Av. Providencia", street_number: "1234", latitude: -33.4489, longitude: -70.6693 },
],
users: [
{
external_id: "user-001",
name: "María García",
roles: ["OWNER"],
contacts: [
{ contact_type: "PHONE", value: "+56912345678" },
{ contact_type: "EMAIL", value: "maria@example.com" },
],
},
],
},
{ external_id: "store-002", name: "Downtown Store" },
{ external_id: "store-003", name: "Harbor Store", tax_id: "76123456-7" },
],
}),
},
);
const result = await response.json();import requests
response = requests.post(
"https://api.reten.ai/api/integration/commerces/batch-upsert",
headers={"x-api-key": "YOUR_API_KEY"},
json={
"items": [
{
"external_id": "store-001",
"name": "Main Street Store",
"tax_id": "76123456-7",
"metadata": {"segment": "gold"},
"addresses": [
{"external_id": "addr-001", "address_alias": "Main store", "country_code": "CL", "region": "Región Metropolitana", "commune": "Providencia", "city": "Santiago", "street": "Av. Providencia", "street_number": "1234", "latitude": -33.4489, "longitude": -70.6693},
],
"users": [
{
"external_id": "user-001",
"name": "María García",
"roles": ["OWNER"],
"contacts": [
{"contact_type": "PHONE", "value": "+56912345678"},
{"contact_type": "EMAIL", "value": "maria@example.com"},
],
},
],
},
{"external_id": "store-002", "name": "Downtown Store"},
{"external_id": "store-003", "name": "Harbor Store", "tax_id": "76123456-7"},
],
},
)
result = response.json()Resposta 200 OK
Neste exemplo, store-001 foi criado, store-002 já existia e foi atualizado, e store-003 falhou porque seu tax_id já está em uso por outro comércio:
{
"summary": {
"total": 3,
"created": 1,
"updated": 1,
"failed": 1
},
"errors": [
{
"index": 2,
"identifier": "store-003",
"error": "upsert_error",
"message": "tax_id 76123456-7 already exists in commerce 5d2f8c1e-7b3a-4c9d-9e1f-2a3b4c5d6e7f"
}
]
}Campos da resposta
| Campo | Tipo | Descrição |
|---|---|---|
summary.total | number | Total de items enviados. |
summary.created | number | Items inseridos (comércios novos). |
summary.updated | number | Items atualizados (comércios existentes modificados). |
summary.failed | number | Items que não puderam ser persistidos. |
errors | object[] | Detalhe por item que falhou (vazio se todos tiveram sucesso). |
errors[].index | number | Posição (base 0) do item dentro do array items enviado. |
errors[].identifier | string | O external_id do item que falhou. |
errors[].error | string | Código de erro estável (veja tabela abaixo). |
errors[].message | string | Mensagem legível para humanos/logs com a causa. |
Códigos de erro por item
Dentro de um item eles são avaliados nesta ordem: duplicado, depois EMAIL inválido, depois coordenadas.
error | Significado |
|---|---|
duplicate_in_batch | O external_id do item já apareceu em um item anterior do mesmo array items. A primeira ocorrência é processada; todas as seguintes falham. |
invalid_contact_value | Um contato do tipo EMAIL traz um value que não é um endereço de e-mail. O item inteiro falha — o comércio, seus endereços e seus usuários — enquanto o resto do lote é gravado. |
upsert_error | O item não pôde ser persistido; message traz a causa (p. ex. uma coordenada fora do intervalo, ou um tax_id já em uso por outro comércio). |
Erros (da request completa)
| Status | Descrição |
|---|---|
400 | items vazio ou com mais de 500 elementos; addresses ou users com mais de 20 elementos, users[].roles com mais de 10, ou users[].contacts com mais de 20; um item que não cumpre o formato (campo obrigatório ausente, tipo ou valor de enum inválido); ou uma propriedade não declarada aqui. Nenhum item é gravado. |
401 | Chave de API ausente ou inválida. |
403 | A chave de API não tem a permissão commerce:batch-upsert. |