OTL ShoesAPI
Exemplos em
Menu da documentação

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.
  • saldo menor 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);
}
As três formas de pagar duas vezes

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