OTL ShoesAPI
Exemplos em
Menu da documentação

Da venda no marketplace ao pedido na OTL

Uma venda entra na Shopee, no Mercado Livre ou na sua loja, e o seu sistema fecha o pedido na OTL sozinho — sem criar duas vezes e sem vender o que não tem.

A ideia

  1. A venda chega ao seu sistema (webhook do marketplace, consulta periódica, pedido da sua loja).
  2. Você traduz os itens para SKU e numeração da OTL.
  3. Você simula o pedido, para saber se dá para atender.
  4. Você cria o pedido, com duas proteções contra duplicidade.
  5. Você acompanha o pedido por webhook e devolve o rastreio ao marketplace.

Permissões da integração: pedidos:ler e pedidos:criar. Para ser avisado do andamento, um webhook assinando os eventos de pedido.

1. Traduza o item

No pedido, cada item é o sku do produto e a numeracao do par — não o SKU da variação:

No marketplace Na OTL
Anúncio “Tênis Forum Low Branco”, variação 38 sku: "320", numeracao: "38"
SKU do anúncio 320U o mesmo par: 320 + numeração 38

Se o seu anúncio guarda o SKU da variação (320U), a tabela de numerações converte o sufixo de volta para a numeração. O mais simples é gravar, no cadastro do anúncio, o sku e a numeracao da OTL quando você o cria.

2. Simule antes de criar

Simular pedido faz todas as conferências — estoque, ficha do parceiro, destinatário — e não cria nada. Use quando a venda ainda pode ser recusada do seu lado (um checkout próprio, por exemplo).

curl -X POST "https://api.otlshoes.com.br/v1/pedidos/validar" \
  -H "Authorization: Bearer otl_prod_SEU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "itens": [
    {
      "sku": "320",
      "numeracao": "38",
      "quantidade": 1
    }
  ],
  "enviarPara": "CLIENTE",
  "novoCliente": {
    "nome": "Maria da Silva",
    "whatsapp": "51999990000",
    "endereco": {
      "cep": "90000-000",
      "rua": "Rua das Flores",
      "numero": "10",
      "bairro": "Centro",
      "cidade": "Porto Alegre",
      "uf": "RS"
    }
  }
}'
curl -X POST "https://api-sandbox.otlshoes.com.br/v1/pedidos/validar" \
  -H "Authorization: Bearer otl_sbx_SEU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "itens": [
    {
      "sku": "320",
      "numeracao": "38",
      "quantidade": 1
    }
  ],
  "enviarPara": "CLIENTE",
  "novoCliente": {
    "nome": "Maria da Silva",
    "whatsapp": "51999990000",
    "endereco": {
      "cep": "90000-000",
      "rua": "Rua das Flores",
      "numero": "10",
      "bairro": "Centro",
      "cidade": "Porto Alegre",
      "uf": "RS"
    }
  }
}'
const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos/validar', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer otl_prod_SEU_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "itens": [
      {
        "sku": "320",
        "numeracao": "38",
        "quantidade": 1
      }
    ],
    "enviarPara": "CLIENTE",
    "novoCliente": {
      "nome": "Maria da Silva",
      "whatsapp": "51999990000",
      "endereco": {
        "cep": "90000-000",
        "rua": "Rua das Flores",
        "numero": "10",
        "bairro": "Centro",
        "cidade": "Porto Alegre",
        "uf": "RS"
      }
    }
  }),
});

if (!resposta.ok) {
  const { erro } = await resposta.json();
  throw new Error(`${erro.codigo}: ${erro.mensagem} (${erro.requestId})`);
}
const dados = await resposta.json();
const resposta = await fetch('https://api-sandbox.otlshoes.com.br/v1/pedidos/validar', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer otl_sbx_SEU_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "itens": [
      {
        "sku": "320",
        "numeracao": "38",
        "quantidade": 1
      }
    ],
    "enviarPara": "CLIENTE",
    "novoCliente": {
      "nome": "Maria da Silva",
      "whatsapp": "51999990000",
      "endereco": {
        "cep": "90000-000",
        "rua": "Rua das Flores",
        "numero": "10",
        "bairro": "Centro",
        "cidade": "Porto Alegre",
        "uf": "RS"
      }
    }
  }),
});

if (!resposta.ok) {
  const { erro } = await resposta.json();
  throw new Error(`${erro.codigo}: ${erro.mensagem} (${erro.requestId})`);
}
const dados = await resposta.json();
<?php
$ch = curl_init('https://api.otlshoes.com.br/v1/pedidos/validar');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer otl_prod_SEU_TOKEN',
    'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'itens' => [
        [
            'sku' => '320',
            'numeracao' => '38',
            'quantidade' => 1,
        ],
    ],
    'enviarPara' => 'CLIENTE',
    'novoCliente' => [
        'nome' => 'Maria da Silva',
        'whatsapp' => '51999990000',
        'endereco' => [
            'cep' => '90000-000',
            'rua' => 'Rua das Flores',
            'numero' => '10',
            'bairro' => 'Centro',
            'cidade' => 'Porto Alegre',
            'uf' => 'RS',
        ],
    ],
]));

$corpo = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status >= 400) {
    throw new Exception($corpo['erro']['codigo'] . ': ' . $corpo['erro']['mensagem']);
}
<?php
$ch = curl_init('https://api-sandbox.otlshoes.com.br/v1/pedidos/validar');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer otl_sbx_SEU_TOKEN',
    'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'itens' => [
        [
            'sku' => '320',
            'numeracao' => '38',
            'quantidade' => 1,
        ],
    ],
    'enviarPara' => 'CLIENTE',
    'novoCliente' => [
        'nome' => 'Maria da Silva',
        'whatsapp' => '51999990000',
        'endereco' => [
            'cep' => '90000-000',
            'rua' => 'Rua das Flores',
            'numero' => '10',
            'bairro' => 'Centro',
            'cidade' => 'Porto Alegre',
            'uf' => 'RS',
        ],
    ],
]));

$corpo = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status >= 400) {
    throw new Exception($corpo['erro']['codigo'] . ': ' . $corpo['erro']['mensagem']);
}
import requests

resposta = requests.post(
    "https://api.otlshoes.com.br/v1/pedidos/validar",
    headers={
        "Authorization": "Bearer otl_prod_SEU_TOKEN",
    },
    json={
        "itens": [
            {
                "sku": "320",
                "numeracao": "38",
                "quantidade": 1
            }
        ],
        "enviarPara": "CLIENTE",
        "novoCliente": {
            "nome": "Maria da Silva",
            "whatsapp": "51999990000",
            "endereco": {
                "cep": "90000-000",
                "rua": "Rua das Flores",
                "numero": "10",
                "bairro": "Centro",
                "cidade": "Porto Alegre",
                "uf": "RS"
            }
        }
    },
    timeout=30,
)

if not resposta.ok:
    erro = resposta.json()["erro"]
    raise RuntimeError(f'{erro["codigo"]}: {erro["mensagem"]} ({erro["requestId"]})')
dados = resposta.json()
import requests

resposta = requests.post(
    "https://api-sandbox.otlshoes.com.br/v1/pedidos/validar",
    headers={
        "Authorization": "Bearer otl_sbx_SEU_TOKEN",
    },
    json={
        "itens": [
            {
                "sku": "320",
                "numeracao": "38",
                "quantidade": 1
            }
        ],
        "enviarPara": "CLIENTE",
        "novoCliente": {
            "nome": "Maria da Silva",
            "whatsapp": "51999990000",
            "endereco": {
                "cep": "90000-000",
                "rua": "Rua das Flores",
                "numero": "10",
                "bairro": "Centro",
                "cidade": "Porto Alegre",
                "uf": "RS"
            }
        }
    },
    timeout=30,
)

if not resposta.ok:
    erro = resposta.json()["erro"]
    raise RuntimeError(f'{erro["codigo"]}: {erro["mensagem"]} ({erro["requestId"]})')
dados = resposta.json()
Testar agora no ambiente de testes

A chamada é feita de verdade, do seu navegador para a API de testes (https://api-sandbox.otlshoes.com.br/v1). Use um token otl_sbx_…: ele fica só nesta aba, e some ao fechá-la. Token de produção não é aceito aqui.

No marketplace a venda já aconteceu

Quando o pedido vem de um marketplace, o cliente já pagou: não há o que simular para decidir. Vá direto para a criação e trate o erro de estoque (passo 4). O que evita vender sem ter é manter o anúncio em dia — veja Pausar o anúncio quando o estoque zera.

3. Crie o pedido

curl -X POST "https://api.otlshoes.com.br/v1/pedidos" \
  -H "Authorization: Bearer otl_prod_SEU_TOKEN" \
  -H "Idempotency-Key: 7f3c1b9e-pedido-ml-123" \
  -H "Content-Type: application/json" \
  -d '{
  "itens": [
    {
      "sku": "320",
      "numeracao": "38",
      "quantidade": 1,
      "valorVenda": "259.90"
    }
  ],
  "enviarPara": "CLIENTE",
  "novoCliente": {
    "nome": "Maria da Silva",
    "whatsapp": "51999990000",
    "endereco": {
      "cep": "90000-000",
      "rua": "Rua das Flores",
      "numero": "10",
      "bairro": "Centro",
      "cidade": "Porto Alegre",
      "uf": "RS"
    }
  },
  "referenciaExterna": "SHOPEE-240929ABC123"
}'
curl -X POST "https://api-sandbox.otlshoes.com.br/v1/pedidos" \
  -H "Authorization: Bearer otl_sbx_SEU_TOKEN" \
  -H "Idempotency-Key: 7f3c1b9e-pedido-ml-123" \
  -H "Content-Type: application/json" \
  -d '{
  "itens": [
    {
      "sku": "320",
      "numeracao": "38",
      "quantidade": 1,
      "valorVenda": "259.90"
    }
  ],
  "enviarPara": "CLIENTE",
  "novoCliente": {
    "nome": "Maria da Silva",
    "whatsapp": "51999990000",
    "endereco": {
      "cep": "90000-000",
      "rua": "Rua das Flores",
      "numero": "10",
      "bairro": "Centro",
      "cidade": "Porto Alegre",
      "uf": "RS"
    }
  },
  "referenciaExterna": "SHOPEE-240929ABC123"
}'
const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer otl_prod_SEU_TOKEN',
    'Idempotency-Key': '7f3c1b9e-pedido-ml-123',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "itens": [
      {
        "sku": "320",
        "numeracao": "38",
        "quantidade": 1,
        "valorVenda": "259.90"
      }
    ],
    "enviarPara": "CLIENTE",
    "novoCliente": {
      "nome": "Maria da Silva",
      "whatsapp": "51999990000",
      "endereco": {
        "cep": "90000-000",
        "rua": "Rua das Flores",
        "numero": "10",
        "bairro": "Centro",
        "cidade": "Porto Alegre",
        "uf": "RS"
      }
    },
    "referenciaExterna": "SHOPEE-240929ABC123"
  }),
});

if (!resposta.ok) {
  const { erro } = await resposta.json();
  throw new Error(`${erro.codigo}: ${erro.mensagem} (${erro.requestId})`);
}
const dados = await resposta.json();
const resposta = await fetch('https://api-sandbox.otlshoes.com.br/v1/pedidos', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer otl_sbx_SEU_TOKEN',
    'Idempotency-Key': '7f3c1b9e-pedido-ml-123',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "itens": [
      {
        "sku": "320",
        "numeracao": "38",
        "quantidade": 1,
        "valorVenda": "259.90"
      }
    ],
    "enviarPara": "CLIENTE",
    "novoCliente": {
      "nome": "Maria da Silva",
      "whatsapp": "51999990000",
      "endereco": {
        "cep": "90000-000",
        "rua": "Rua das Flores",
        "numero": "10",
        "bairro": "Centro",
        "cidade": "Porto Alegre",
        "uf": "RS"
      }
    },
    "referenciaExterna": "SHOPEE-240929ABC123"
  }),
});

if (!resposta.ok) {
  const { erro } = await resposta.json();
  throw new Error(`${erro.codigo}: ${erro.mensagem} (${erro.requestId})`);
}
const dados = await resposta.json();
<?php
$ch = curl_init('https://api.otlshoes.com.br/v1/pedidos');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer otl_prod_SEU_TOKEN',
    'Idempotency-Key: 7f3c1b9e-pedido-ml-123',
    'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'itens' => [
        [
            'sku' => '320',
            'numeracao' => '38',
            'quantidade' => 1,
            'valorVenda' => '259.90',
        ],
    ],
    'enviarPara' => 'CLIENTE',
    'novoCliente' => [
        'nome' => 'Maria da Silva',
        'whatsapp' => '51999990000',
        'endereco' => [
            'cep' => '90000-000',
            'rua' => 'Rua das Flores',
            'numero' => '10',
            'bairro' => 'Centro',
            'cidade' => 'Porto Alegre',
            'uf' => 'RS',
        ],
    ],
    'referenciaExterna' => 'SHOPEE-240929ABC123',
]));

$corpo = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status >= 400) {
    throw new Exception($corpo['erro']['codigo'] . ': ' . $corpo['erro']['mensagem']);
}
<?php
$ch = curl_init('https://api-sandbox.otlshoes.com.br/v1/pedidos');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer otl_sbx_SEU_TOKEN',
    'Idempotency-Key: 7f3c1b9e-pedido-ml-123',
    'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'itens' => [
        [
            'sku' => '320',
            'numeracao' => '38',
            'quantidade' => 1,
            'valorVenda' => '259.90',
        ],
    ],
    'enviarPara' => 'CLIENTE',
    'novoCliente' => [
        'nome' => 'Maria da Silva',
        'whatsapp' => '51999990000',
        'endereco' => [
            'cep' => '90000-000',
            'rua' => 'Rua das Flores',
            'numero' => '10',
            'bairro' => 'Centro',
            'cidade' => 'Porto Alegre',
            'uf' => 'RS',
        ],
    ],
    'referenciaExterna' => 'SHOPEE-240929ABC123',
]));

$corpo = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status >= 400) {
    throw new Exception($corpo['erro']['codigo'] . ': ' . $corpo['erro']['mensagem']);
}
import requests

resposta = requests.post(
    "https://api.otlshoes.com.br/v1/pedidos",
    headers={
        "Authorization": "Bearer otl_prod_SEU_TOKEN",
        "Idempotency-Key": "7f3c1b9e-pedido-ml-123",
    },
    json={
        "itens": [
            {
                "sku": "320",
                "numeracao": "38",
                "quantidade": 1,
                "valorVenda": "259.90"
            }
        ],
        "enviarPara": "CLIENTE",
        "novoCliente": {
            "nome": "Maria da Silva",
            "whatsapp": "51999990000",
            "endereco": {
                "cep": "90000-000",
                "rua": "Rua das Flores",
                "numero": "10",
                "bairro": "Centro",
                "cidade": "Porto Alegre",
                "uf": "RS"
            }
        },
        "referenciaExterna": "SHOPEE-240929ABC123"
    },
    timeout=30,
)

if not resposta.ok:
    erro = resposta.json()["erro"]
    raise RuntimeError(f'{erro["codigo"]}: {erro["mensagem"]} ({erro["requestId"]})')
dados = resposta.json()
import requests

resposta = requests.post(
    "https://api-sandbox.otlshoes.com.br/v1/pedidos",
    headers={
        "Authorization": "Bearer otl_sbx_SEU_TOKEN",
        "Idempotency-Key": "7f3c1b9e-pedido-ml-123",
    },
    json={
        "itens": [
            {
                "sku": "320",
                "numeracao": "38",
                "quantidade": 1,
                "valorVenda": "259.90"
            }
        ],
        "enviarPara": "CLIENTE",
        "novoCliente": {
            "nome": "Maria da Silva",
            "whatsapp": "51999990000",
            "endereco": {
                "cep": "90000-000",
                "rua": "Rua das Flores",
                "numero": "10",
                "bairro": "Centro",
                "cidade": "Porto Alegre",
                "uf": "RS"
            }
        },
        "referenciaExterna": "SHOPEE-240929ABC123"
    },
    timeout=30,
)

if not resposta.ok:
    erro = resposta.json()["erro"]
    raise RuntimeError(f'{erro["codigo"]}: {erro["mensagem"]} ({erro["requestId"]})')
dados = resposta.json()
Testar agora no ambiente de testes

A chamada é feita de verdade, do seu navegador para a API de testes (https://api-sandbox.otlshoes.com.br/v1). Use um token otl_sbx_…: ele fica só nesta aba, e some ao fechá-la. Token de produção não é aceito aqui.

Três campos fazem a diferença aqui:

  • referenciaExterna — o número do pedido no marketplace. É o que liga os dois sistemas e a segunda proteção contra duplicidade.
  • valorVenda — por quanto você vendeu, por par. Opcional no pedido, mas obrigatório para emitir etiqueta depois, e é o que calcula o lucro do pedido.
  • novoCliente — o destinatário vai junto, sem precisar cadastrá-lo antes. Com salvarCliente: true ele também entra na agenda; em venda de marketplace normalmente não vale a pena guardar.

4. Não crie o pedido duas vezes

É o erro que mais custa caro, e a API tem duas travas. Use as duas.

import { randomUUID } from 'node:crypto';

async function fecharPedido(venda) {
  // UMA chave por venda, gravada ANTES de chamar. Se o processo cair e a venda
  // for reprocessada, a mesma chave é reusada.
  const chave = venda.chaveOtl ?? (await salvarChave(venda.id, randomUUID()));

  for (let tentativa = 1; tentativa <= 4; tentativa++) {
    const resposta = await fetch(`${BASE}/pedidos`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${TOKEN}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': chave,
      },
      body: JSON.stringify(montarPedido(venda)),
    }).catch(() => null); // rede caiu: tenta de novo com a MESMA chave

    if (!resposta || resposta.status >= 500 || resposta.status === 429) {
      const espera = Number(resposta?.headers.get('Retry-After') ?? 2 ** tentativa);
      await new Promise(r => setTimeout(r, espera * 1000));
      continue;
    }

    const corpo = await resposta.json();
    if (resposta.ok) return corpo; // 201: criado (ou o mesmo de antes, se a chave se repetiu)

    // A venda já virou pedido por outro caminho: use o pedido que existe.
    if (corpo.erro.codigo === 'REFERENCIA_EXTERNA_DUPLICADA') return corpo.erro.pedido;

    throw Object.assign(new Error(corpo.erro.mensagem), { erro: corpo.erro });
  }
  throw new Error('A OTL não respondeu. A venda fica na fila para nova tentativa.');
}
  • Timeout ou erro de rede não quer dizer que o pedido não foi criado. Repita com a mesma Idempotency-Key: se ele existe, você o recebe de volta.
  • Chave nova a cada tentativa anula a proteção. Por isso ela é gravada junto da venda, antes da primeira chamada.
  • 409 REFERENCIA_EXTERNA_DUPLICADA traz erro.pedido com o id e o numero do pedido que já existe. Não é falha: é a trava funcionando.

5. Quando não dá para atender

422 ITENS_INDISPONIVEIS lista todos os itens com problema, cada um com o indice no seu corpo, o motivo e quanto há disponivel. O pedido inteiro é recusado: não existe pedido parcial.

O que fazer depende do seu negócio — cancelar a venda no marketplace, avisar o cliente, oferecer outra numeração. O que não fazer é tentar de novo em laço: o estoque não volta sozinho.

Outros erros que pedem uma pessoa, não uma nova tentativa:

codigo O que é
PROFILE_INCOMPLETE A ficha do parceiro está incompleta. Ele completa no painel.
VALIDACAO O corpo tem problema — erro.detalhes diz o campo. Endereço incompleto é o mais comum.
ESCOPO_INSUFICIENTE A integração não tem pedidos:criar.

6. Acompanhe o pedido

O pedido nasce AGUARDANDO_PAGAMENTO: o parceiro acerta o pagamento com a OTL, e o status avança. Em vez de consultar, assine os webhooks:

Evento O que fazer do seu lado
pedido.status_alterado Atualizar o status no seu sistema. ENVIADO é o momento de avisar o cliente.
pedido.rastreio_adicionado Mandar o código de rastreio para o marketplace.
pedido.comprovante_validado O pagamento do parceiro foi conferido.

O corpo do webhook traz o resumo do pedido, com a referenciaExterna — é por ela que você acha a venda no seu sistema. Para o detalhe, chame Detalhar pedido.

Como reserva (webhook pode falhar), rode de tempos em tempos Listar pedidos com atualizadoDesde.

Teste o fluxo inteiro no sandbox

  1. Crie o pedido com o token de teste.
  2. Use os simuladores para fazer o papel da OTL: status PAGO, depois ENVIADO.
  3. Confira que os webhooks chegaram e que o seu sistema reagiu.
  4. Repita a criação com a mesma Idempotency-Key e veja que nenhum pedido novo aparece.

Ver também