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
- A venda chega ao seu sistema (webhook do marketplace, consulta periódica, pedido da sua loja).
- Você traduz os itens para SKU e numeração da OTL.
- Você simula o pedido, para saber se dá para atender.
- Você cria o pedido, com duas proteções contra duplicidade.
- 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.
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. ComsalvarCliente: trueele 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_DUPLICADAtrazerro.pedidocom oide onumerodo 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
- Crie o pedido com o token de teste.
- Use os simuladores para fazer o papel da OTL: status
PAGO, depoisENVIADO. - Confira que os webhooks chegaram e que o seu sistema reagiu.
- Repita a criação com a mesma
Idempotency-Keye veja que nenhum pedido novo aparece.
Ver também
- Criar pedido — todos os campos e erros
- Emitir etiqueta sem cobrança duplicada
- Boas práticas
