Emitir etiqueta sem cobrança duplicada
A etiqueta custa dinheiro do parceiro no instante em que é emitida. Este guia mostra a ordem certa das chamadas e como repetir uma tentativa sem pagar duas vezes.
Como funciona
A mercadoria é da OTL, mas o frete é do parceiro: a etiqueta é emitida na conta SuperFrete dele, com o saldo dele. Depois de emitida, ela entra na fila da equipe OTL, que imprime, cola na caixa e despacha.
Emitir paga na hora. Não existe “criar sem pagar”, e é por isso que este guia existe.
Permissões da integração: etiquetas:ler e etiquetas:emitir (e pedidos:escrever, se o seu
sistema informa o valor de venda).
1. Antes de tudo: dá para emitir?
curl "https://api.otlshoes.com.br/v1/superfrete" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN"curl "https://api-sandbox.otlshoes.com.br/v1/superfrete" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN"const resposta = await fetch('https://api.otlshoes.com.br/v1/superfrete', {
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/superfrete', {
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/superfrete');
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/superfrete');
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/superfrete",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
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/superfrete",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
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.
conectada: false— o parceiro precisa conectar a conta SuperFrete no painel. A API não faz isso por ele. Pare aqui e avise.saldomenor que o frete — a emissão vai falhar. Avise antes de tentar.saldo: null— não deu para consultar agora. Não é saldo zero: siga, e trate o erro se vier.
2. Garanta o valor de venda dos itens
A declaração de conteúdo, que vai colada na caixa, usa o valor de venda de cada item — nunca o
que o parceiro pagou à OTL. Item sem valor de venda bloqueia a emissão (SALE_VALUE_REQUIRED).
Se você não informou valorVenda ao criar o pedido, informe agora:
curl -X PATCH "https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"valorVenda": "259.90"
}'curl -X PATCH "https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"valorVenda": "259.90"
}'const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda', {
method: 'PATCH',
headers: {
Authorization: 'Bearer otl_prod_SEU_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"valorVenda": "259.90"
}),
});
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/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda', {
method: 'PATCH',
headers: {
Authorization: 'Bearer otl_sbx_SEU_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"valorVenda": "259.90"
}),
});
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/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer otl_prod_SEU_TOKEN',
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'valorVenda' => '259.90',
]));
$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/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer otl_sbx_SEU_TOKEN',
'Content-Type: application/json',
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'valorVenda' => '259.90',
]));
$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.patch(
"https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
json={
"valorVenda": "259.90"
},
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.patch(
"https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/itens/cmg8k1p2a0008/valor-venda",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
json={
"valorVenda": "259.90"
},
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.
3. Cote
curl "https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cotacao" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN"curl "https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cotacao" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN"const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cotacao', {
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/pedidos/cmg8k1p2a0007/etiquetas/cotacao', {
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/pedidos/cmg8k1p2a0007/etiquetas/cotacao');
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/pedidos/cmg8k1p2a0007/etiquetas/cotacao');
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/pedidos/cmg8k1p2a0007/etiquetas/cotacao",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
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/pedidos/cmg8k1p2a0007/etiquetas/cotacao",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
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.
A cotação não consome saldo. Escolha um servico.id da resposta — a lista vem da mais barata para a
mais cara, e serviço sem cobertura para o CEP nem aparece. Lista vazia: não há como enviar por ali.
4. Emita — com a chave gravada antes
curl -X POST "https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN" \
-H "Idempotency-Key: 7f3c1b9e-pedido-ml-123" \
-H "Content-Type: application/json" \
-d '{
"servico": 1,
"documentoDestinatario": "529.982.247-25"
}'curl -X POST "https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN" \
-H "Idempotency-Key: 7f3c1b9e-pedido-ml-123" \
-H "Content-Type: application/json" \
-d '{
"servico": 1,
"documentoDestinatario": "529.982.247-25"
}'const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas', {
method: 'POST',
headers: {
Authorization: 'Bearer otl_prod_SEU_TOKEN',
'Idempotency-Key': '7f3c1b9e-pedido-ml-123',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"servico": 1,
"documentoDestinatario": "529.982.247-25"
}),
});
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/cmg8k1p2a0007/etiquetas', {
method: 'POST',
headers: {
Authorization: 'Bearer otl_sbx_SEU_TOKEN',
'Idempotency-Key': '7f3c1b9e-pedido-ml-123',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"servico": 1,
"documentoDestinatario": "529.982.247-25"
}),
});
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/cmg8k1p2a0007/etiquetas');
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([
'servico' => 1,
'documentoDestinatario' => '529.982.247-25',
]));
$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/cmg8k1p2a0007/etiquetas');
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([
'servico' => 1,
'documentoDestinatario' => '529.982.247-25',
]));
$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/cmg8k1p2a0007/etiquetas",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
"Idempotency-Key": "7f3c1b9e-pedido-ml-123",
},
json={
"servico": 1,
"documentoDestinatario": "529.982.247-25"
},
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/cmg8k1p2a0007/etiquetas",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
"Idempotency-Key": "7f3c1b9e-pedido-ml-123",
},
json={
"servico": 1,
"documentoDestinatario": "529.982.247-25"
},
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.
A Idempotency-Key é obrigatória aqui, e o jeito de usá-la é o que separa uma etiqueta paga de
duas:
import { randomUUID } from 'node:crypto';
async function emitirEtiqueta(pedido, servico, documento) {
// 1. A chave é gerada UMA vez por etiqueta e gravada ANTES da chamada.
const chave = pedido.chaveEtiqueta ?? (await salvarChaveEtiqueta(pedido.id, randomUUID()));
for (let tentativa = 1; tentativa <= 4; tentativa++) {
const resposta = await fetch(`${BASE}/pedidos/${pedido.idOtl}/etiquetas`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': chave,
},
body: JSON.stringify({ servico, documentoDestinatario: documento }),
}).catch(() => null);
// 2. Sem resposta, 5xx ou 429: NÃO se sabe se pagou. Repete com a MESMA chave.
if (!resposta || resposta.status >= 500 || resposta.status === 429) {
await new Promise(r => setTimeout(r, 2 ** tentativa * 1000));
continue;
}
const corpo = await resposta.json();
if (resposta.ok) return corpo; // a etiqueta — a nova, ou a mesma de antes
// 3. Erro 4xx: nada foi pago. Não adianta repetir sem resolver a causa.
throw Object.assign(new Error(corpo.erro.mensagem), { erro: corpo.erro });
}
// 4. Esgotou as tentativas: confira antes de qualquer outra coisa (passo 5).
return conferir(pedido);
}
Gerar uma chave nova a cada tentativa. A chave é o que diz à OTL “é a mesma etiqueta”. Chave nova = etiqueta nova.
Tratar timeout como “não foi”. Quando a chamada não responde, você não sabe se a etiqueta foi paga. Repita com a mesma chave, ou confira pela lista — nunca emita de novo às cegas.
Deixar o usuário clicar duas vezes. Se a emissão parte de um botão, desabilite-o no primeiro clique e use a chave gravada no pedido, não uma gerada no clique.
Além da chave, há uma segunda trava do lado da OTL: um pedido tem no máximo uma etiqueta ativa. Uma segunda emissão para o mesmo pedido é recusada — mesmo com outra chave — enquanto a primeira não for cancelada.
5. Na dúvida, confira
Se o seu sistema perdeu o controle (caiu no meio, esgotou as tentativas), a verdade está aqui:
curl "https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN"curl "https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN"const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas', {
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/pedidos/cmg8k1p2a0007/etiquetas', {
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/pedidos/cmg8k1p2a0007/etiquetas');
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/pedidos/cmg8k1p2a0007/etiquetas');
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/pedidos/cmg8k1p2a0007/etiquetas",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
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/pedidos/cmg8k1p2a0007/etiquetas",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
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.
async function conferir(pedido) {
const { etiquetas } = await chamar(`/pedidos/${pedido.idOtl}/etiquetas`);
// Ativa = qualquer status que não seja CANCELADA.
return etiquetas.find(e => e.status !== 'CANCELADA') ?? null;
}
Existe uma etiqueta ativa → ela foi paga: grave-a e siga. Não existe → nada foi cobrado, e você pode emitir de novo (com a mesma chave, se ainda estiver dentro de 24 horas).
6. O documento do destinatário
A transportadora exige o CPF ou CNPJ de quem recebe. A OTL não guarda esse dado: ele é repassado e descartado, e por isso é pedido a cada emissão.
- Pedido para um cliente: envie
documentoDestinatario. - Pedido para o próprio parceiro: não precisa — é usado o CPF da ficha dele.
Guarde o documento no seu sistema, junto da venda, pelo tempo que a sua política de dados permitir.
7. Acompanhe
A etiqueta nasce GERADA (paga, esperando a OTL imprimir) e segue para IMPRESSA e DESPACHADA.
Assine os webhooks em vez de consultar:
| Evento | Quando |
|---|---|
etiqueta.emitida |
A etiqueta foi paga — pelo painel ou pela API |
etiqueta.status_alterado |
Impressa, despachada ou cancelada. Traz o statusAnterior |
pedido.rastreio_adicionado |
O código de rastreio entrou no pedido |
O rastreio pode vir null na resposta da emissão: a transportadora às vezes demora a devolvê-lo.
Ele chega depois, pelo webhook de rastreio.
8. Cancelar
curl -X DELETE "https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003" \
-H "Authorization: Bearer otl_prod_SEU_TOKEN"curl -X DELETE "https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003" \
-H "Authorization: Bearer otl_sbx_SEU_TOKEN"const resposta = await fetch('https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003', {
method: 'DELETE',
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/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003', {
method: 'DELETE',
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/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
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/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
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.delete(
"https://api.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003",
headers={
"Authorization": "Bearer otl_prod_SEU_TOKEN",
},
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.delete(
"https://api-sandbox.otlshoes.com.br/v1/pedidos/cmg8k1p2a0007/etiquetas/cmg8p2r5v0003",
headers={
"Authorization": "Bearer otl_sbx_SEU_TOKEN",
},
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 valor volta para a carteira SuperFrete do parceiro. Só dá para cancelar antes de despachada; se ela já foi impressa, avise a equipe OTL, porque o papel já existe. Depois de cancelar, o pedido aceita uma etiqueta nova — com uma chave nova, porque agora é mesmo outra etiqueta.
Quando a emissão é recusada
codigo |
O que fazer |
|---|---|
SUPERFRETE_NOT_CONNECTED |
O parceiro conecta a conta no painel |
SALE_VALUE_REQUIRED |
Informe o valor de venda dos itens (passo 2) |
VALIDACAO |
servico fora da lista ou documento inválido — erro.detalhes diz qual |
REQUISICAO_INVALIDA |
O pedido já tem etiqueta ativa, está cancelado, ou falta endereço — a mensagem diz |
ESCOPO_INSUFICIENTE |
A integração não tem etiquetas:emitir |
Nenhum desses cobrou nada. Resolva a causa antes de tentar de novo.
Teste no sandbox
No ambiente de testes a etiqueta é paga com saldo fictício da SuperFrete de testes — a conta de teste precisa estar conectada a ela, pelo painel de testes. Depois de emitir, use o simulador de status da etiqueta para fazer o papel da OTL imprimindo e despachando, e veja os webhooks chegarem.
Vale testar de propósito: derrube a sua conexão no meio de uma emissão, repita com a mesma chave e confira que existe uma etiqueta.
Ver também
- Emitir etiqueta — todos os campos e erros
- Idempotência
- Da venda no marketplace ao pedido na OTL
