Reten Docs
Comércios

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 mesmo external_id. Se nenhum o carrega, um endereço novo é criado e a heurística abaixo não é tentada.
  • Sem external_id (ausente, null ou "") — duas heurísticas são tentadas em ordem: primeiro um endereço com o mesmo formatted_address, só se o item enviar formatted_address; depois um endereço com os mesmos street, street_number e commune, 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

CampoTipoObrigatórioDescrição
itemsobject[]SimLista de comércios a gravar (mínimo 1, máximo 500).

Cada elemento de items:

CampoTipoObrigatórioDescrição
external_idstringSimIdentificador do comércio no sistema externo. Chave do upsert.
namestringSimNome do comércio.
global_external_idstringNãoIdentificador externo adicional do comércio. Não é usado para a correspondência.
tax_idstringNãoIdentificador tributário. Não pode estar em uso por outro comércio do tenant. "" equivale a null e o apaga.
metadataobjectNãoJSON arbitrário. Mesclado recursivamente com a metadata existente na atualização.
addressesobject[]NãoEndereços do comércio (máximo 20, veja abaixo).
usersobject[]NãoUsuários do comércio (máximo 20, veja abaixo).

Cada elemento de addresses (todos os campos são opcionais):

CampoTipoObrigatórioDescrição
external_idstringNãoIdentificador do endereço no sistema externo. Chave de correspondência — veja Correspondência de endereços.
address_aliasstringNãoRótulo do endereço (p. ex. "Loja matriz").
is_primarybooleanNãoDescontinuado — aceito, porém ignorado.
country_codestringNãoCódigo do país.
regionstringNãoRegião / estado.
communestringNãoComuna / município.
citystringNãoCidade.
streetstringNãoNome da rua.
street_numberstringNãoNúmero.
unitstringNãoUnidade, andar ou sala.
formatted_addressstringNãoEndereço completo em uma única string.
latitudenumberNãoLatitude, entre -90 e 90 inclusive. Fora do intervalo manda o item inteiro para errors.
longitudenumberNãoLongitude, entre -180 e 180 inclusive. Fora do intervalo manda o item inteiro para errors.

Cada elemento de users:

CampoTipoObrigatórioDescrição
external_idstringSimIdentificador do usuário no sistema externo.
namestringNãoNome do usuário.
rolesenum[]NãoFunções do usuário no comércio (máximo 10): OWNER, ORDERS, FINANCE, ATTENTION, ADMIN, COMMERCE_REP, OTHER.
contactsobject[]NãoContatos do usuário (máximo 20, veja abaixo).

Cada elemento de users[].contacts:

CampoTipoObrigatórioDescrição
contact_typeenumSimTipo de contato: PHONE, EMAIL, WHATSAPP ou OTHER.
valuestringSimValor do contato. Apenas EMAIL tem o formato validado; PHONE, WHATSAPP e OTHER são gravados como enviados.
is_primarybooleanNãoDescontinuado — 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

CampoTipoDescrição
summary.totalnumberTotal de items enviados.
summary.creatednumberItems inseridos (comércios novos).
summary.updatednumberItems atualizados (comércios existentes modificados).
summary.failednumberItems que não puderam ser persistidos.
errorsobject[]Detalhe por item que falhou (vazio se todos tiveram sucesso).
errors[].indexnumberPosição (base 0) do item dentro do array items enviado.
errors[].identifierstringO external_id do item que falhou.
errors[].errorstringCódigo de erro estável (veja tabela abaixo).
errors[].messagestringMensagem 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.

errorSignificado
duplicate_in_batchO 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_valueUm 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_errorO 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)

StatusDescrição
400items 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.
401Chave de API ausente ou inválida.
403A chave de API não tem a permissão commerce:batch-upsert.