Conceitos
As convenções que valem para a API inteira — dinheiro, datas, identificadores, paginação, limite de chamadas e versões.
Formato
Requisições e respostas em JSON, UTF-8. Os campos têm nome em português, em camelCase:
precoAtacado, atualizadoEm, skuVariacao.
Ignore campos que você não conhece. Campos novos podem ser acrescentados a qualquer momento sem mudança de versão; um sistema que quebra ao ver um campo a mais vai quebrar.
Dinheiro
Sempre texto com duas casas decimais, com ponto: "139.90".
Nunca número. Em ponto flutuante, 139.9 perde o zero e 0.1 + 0.2 não dá 0.3; cada linguagem
arredonda de um jeito. Converta para centavos inteiros (ou para o tipo decimal da sua linguagem)
antes de fazer conta.
Datas
ISO 8601 com fuso, no horário de Brasília: "2026-09-29T14:03:00-03:00".
Nos parâmetros de data (como atualizadoDesde), o fuso é obrigatório. 2026-09-29T14:00:00
sem ele é recusado: não há como saber de que horário você está falando. Aceita-se -03:00 ou Z.
Identificadores
Os campos id são textos opacos. Não assuma formato, tamanho nem ordem, e não tente gerar um.
O identificador que você deve guardar para um produto é o sku, que é o mesmo nos dois
ambientes.
Paginação
As listas são paginadas por cursor:
curl "https://api.otlshoes.com.br/v1/produtos?limite=50&cursor=eyJvIjoicmVjZW50ZXMi%E2%80%A6" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN"curl "https://api-sandbox.otlshoes.com.br/v1/produtos?limite=50&cursor=eyJvIjoicmVjZW50ZXMi%E2%80%A6" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN"const resposta = await fetch('https://api.otlshoes.com.br/v1/produtos?limite=50&cursor=eyJvIjoicmVjZW50ZXMi%E2%80%A6', {
headers: {
Authorization: 'Bearer otl_prod_SEU_TOKEN',
},
});
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/produtos?limite=50&cursor=eyJvIjoicmVjZW50ZXMi%E2%80%A6', {
headers: {
Authorization: 'Bearer otl_sbx_SEU_TOKEN',
},
});
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/produtos?limite=50&cursor=eyJvIjoicmVjZW50ZXMi%E2%80%A6');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer otl_prod_SEU_TOKEN',
]);
$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/produtos?limite=50&cursor=eyJvIjoicmVjZW50ZXMi%E2%80%A6');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer otl_sbx_SEU_TOKEN',
]);
$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.get(
"https://api.otlshoes.com.br/v1/produtos",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
params={
"limite": 50,
"cursor": "eyJvIjoicmVjZW50ZXMi…"
},
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.get(
"https://api-sandbox.otlshoes.com.br/v1/produtos",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
params={
"limite": 50,
"cursor": "eyJvIjoicmVjZW50ZXMi…"
},
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.
{
"dados": [ … ],
"paginacao": { "proximoCursor": "eyJvIjoicmVjZW50ZXMi…", "temMais": true }
}
limitevai de 1 a 100; o padrão é 50.- Para a próxima página, repita a chamada com os mesmos filtros e
cursorigual aoproximoCursorrecebido. - Quando
temMaisforfalse, acabou —proximoCursorvemnull. - O cursor é opaco: não monte, não altere, não guarde por muito tempo. Ele vale para a
ordenação em que foi gerado; usado com outra
ordem, responde400 CURSOR_INVALIDO.
Não há paginação por número de página. Ela pula e repete itens quando o catálogo muda no meio da varredura — um produto novo empurra todos os outros uma posição.
Sincronização incremental
Depois da primeira carga, não baixe tudo de novo: peça só o que mudou, com atualizadoDesde.
curl "https://api.otlshoes.com.br/v1/produtos?atualizadoDesde=2026-09-29T14%3A00%3A00-03%3A00" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN"curl "https://api-sandbox.otlshoes.com.br/v1/produtos?atualizadoDesde=2026-09-29T14%3A00%3A00-03%3A00" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN"const resposta = await fetch('https://api.otlshoes.com.br/v1/produtos?atualizadoDesde=2026-09-29T14%3A00%3A00-03%3A00', {
headers: {
Authorization: 'Bearer otl_prod_SEU_TOKEN',
},
});
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/produtos?atualizadoDesde=2026-09-29T14%3A00%3A00-03%3A00', {
headers: {
Authorization: 'Bearer otl_sbx_SEU_TOKEN',
},
});
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/produtos?atualizadoDesde=2026-09-29T14%3A00%3A00-03%3A00');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer otl_prod_SEU_TOKEN',
]);
$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/produtos?atualizadoDesde=2026-09-29T14%3A00%3A00-03%3A00');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer otl_sbx_SEU_TOKEN',
]);
$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.get(
"https://api.otlshoes.com.br/v1/produtos",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
params={
"atualizadoDesde": "2026-09-29T14:00:00-03:00"
},
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.get(
"https://api-sandbox.otlshoes.com.br/v1/produtos",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
params={
"atualizadoDesde": "2026-09-29T14:00:00-03:00"
},
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.
O detalhe e a receita completa estão em Sincronizar catálogo e estoque.
Limite de chamadas
- 60 chamadas por minuto por parceiro, salvo combinação diferente com a OTL. O limite soma todas as integrações do parceiro: dois tokens não dobram o limite.
- Toda resposta traz
X-RateLimit-Limit,X-RateLimit-RemainingeX-RateLimit-Reset. - Passou do limite:
429 LIMITE_EXCEDIDO, comRetry-Afterdizendo quantos segundos esperar. - Chamadas recusadas por limite não contam no limite.
Espere o tempo do Retry-After e tente de novo. Repetir em seguida só prolonga o bloqueio do seu
próprio sistema.
Idempotência
Para as rotas que criam algo (pedidos e etiquetas, quando estiverem disponíveis), o cabeçalho
Idempotency-Key garante que repetir a chamada não cria de novo.
A rede pode cair depois de o servidor processar e antes de você receber a resposta. Sem a chave, a nova tentativa de “criar pedido” vira um segundo pedido.
- Mande um identificador único por operação — um UUID, ou o número do pedido no seu sistema.
- Repetiu a mesma chamada com a mesma chave: recebe a resposta da primeira vez, com o
cabeçalho
Idempotent-Replayed: true. - Mesma chave com corpo diferente:
422 IDEMPOTENCY_KEY_REUTILIZADA. - A chave vale por 24 horas, por integração.
Rastreio de chamadas
Toda resposta traz X-Request-Id, e nos erros o mesmo valor vem em erro.requestId. Registre-o no
seu log. Ao abrir um chamado, é com ele que a equipe OTL encontra a chamada exata.
Versões
A versão está no caminho: /v1.
- Não mudam a versão: campo novo, filtro novo, rota nova, código de erro novo, permissão nova.
- Muda a versão: remover ou renomear campo, mudar o tipo ou o significado de algo. Isso só
acontece numa
v2, com av1mantida por pelo menos 12 meses depois do anúncio. - Rota que vai ser descontinuada passa a responder com os cabeçalhos
DeprecationeSunset, e aparece no changelog. - Toda mudança sai numa release datada, com um resumo e, quando for o caso, o que o seu sistema
precisa fazer. O changelog explica como ler, e o mesmo conteúdo está em
/changelog.jsonpara acompanhar por sistema.
