Última atualização: 27 de Setembro de 2026

API e webhooks

Leia os pedidos, o catálogo, os clientes e as assinaturas da sua loja, gere uma cobrança avulsa e receba um aviso no seu servidor a cada mudança. Tudo em JSON, autenticado por chave.

Começar

A chave nasce no painel, em Configurações → Integrações. Ela aparece UMA vez, na hora de criar: o Junne guarda só um hash, então não há como mostrá-la de novo — perdeu, cria outra e revoga a antiga.

Toda chamada leva a chave no cabeçalho Authorization. A chave já diz de qual loja são os dados: não existe parâmetro de loja em nenhuma rota, e um id de outra loja responde 404, nunca o dado.

export JUNNE_API_KEY="jn_live_..."   # a sua, criada no painel

curl https://www.junne.app/api/public/v1/pedidos?limite=5 \
  -H "Authorization: Bearer $JUNNE_API_KEY"

A chave numa variável de ambiente, e não colada no comando: o terminal guarda o que você digita, e um curl com a chave dentro fica no histórico do shell — é de lá que ela costuma vazar depois.

A API não movimenta dinheiro. Quase todos os endpoints são de leitura; POST /cobrancas cria uma cobrança avulsa, mas quem paga é sempre o link de pagamento hospedado que a resposta devolve — é por ele que cobrança e assinatura nascem, e o Junne o hospeda.

Escopos

Cada chave carrega os escopos que você marcar, e cada rota exige um. Marque só o que a integração usa: uma chave de conciliação não precisa ler cliente.

  • pedidos.lerLista e detalhe dos pedidos da loja.
  • catalogo.lerProdutos, categorias e adicionais.
  • clientes.lerQuem já comprou nesta loja, sem os endereços salvos.
  • assinaturas.lerPlanos, assinantes e cobranças do clube.
  • catalogo.escreverPreço e disponibilidade dos produtos.ainda sem endpoint
  • pedidos.criarAbrir pedido de entrega ou retirada.ainda sem endpoint
  • cobrancas.criarGerar link de pagamento avulso.
  • assinaturas.criarCriar plano de assinatura. O link de adesão é o id do plano.
  • saldo.linkGerar link temporário que mostra o saldo.ainda sem endpoint
  • conta.lerSe a conta já pode cobrar e por qual meio.

Respostas e erros

Lista devolve { dados, proximaPagina }. Erro devolve { erro, codigo } — e o que sua integração deve tratar é o codigo. A frase em erro é para pessoa ler e pode melhorar a qualquer momento sem aviso; o código, não.

CódigoHTTPQuando
NAO_AUTENTICADO401Chave ausente, malformada ou revogada.
SEM_ESCOPO403A chave existe, mas não tem o escopo daquela rota.
NAO_ENCONTRADO404O id não existe — ou é de outra loja.
PARAMETRO_INVALIDO400Filtro fora do formato (data, estado, limite).
TETO_DE_USO429Limite por minuto ou por dia. Vem com Retry-After.
PEDIDO_INVALIDO422O corpo esta correto, mas a loja recusa este pedido: valor abaixo do minimo dela, documento do pagador faltando, forma de pagamento indisponivel. A mensagem diz qual.
CONTA_INDISPONIVEL422A conta do lojista nao recebe pagamento: suspensa, encerrada, ou a conta de pagamento ainda nao foi aberta (ele precisa concluir a verificacao). A mensagem traz o motivo, pra mostrar a ele. Repetir nao resolve.
PIX_INDISPONIVEL422O Pix so libera depois da aprovacao do cadastro. Cobrar no cartao funciona antes disso: repita com metodo CARD.
ERRO_INTERNO500Falha do nosso lado. Num POST, repetir pode criar uma segunda cobranca — leia a armadilha sobre retry antes de tentar de novo.

Todo valor em dinheiro é inteiro, em centavos, em campos terminados em Cents. Datas são ISO 8601 em UTC.

Paginação

Por cursor, não por página numerada: o cursor é o id da última linha devolvida, então uma linha nova no topo não embaralha o que você já leu.

limite vai de 1 a 200 e o padrão é 50; fora da faixa, o valor é ajustado para a borda mais próxima. Quando proximaPagina vier null, acabou.

curl "https://www.junne.app/api/public/v1/pedidos?limite=100" -H "Authorization: Bearer $JUNNE_API_KEY"
# { "dados": [...], "proximaPagina": "ord_abc123" }

curl "https://www.junne.app/api/public/v1/pedidos?limite=100&cursor=ord_abc123" -H "Authorization: Bearer $JUNNE_API_KEY"

Limites de uso

120 por minuto e 10.000 por dia, contados por chave. Estourar devolve 429 com Retry-After em segundos — respeite o cabeçalho em vez de tentar de novo em laço.

Sincronização inicial de catálogo grande: use limite=200 e siga o cursor, em vez de muitas chamadas pequenas.

Endpoints

  • GET/pedidospedidos.ler

    Os pedidos da loja, do mais recente para o mais antigo.

    • de Data inicial, AAAA-MM-DD. Inclusiva.
    • ate Data final, AAAA-MM-DD. EXCLUSIVA — o dia informado fica de fora.
    • estado Um estado de pedido. Valor desconhecido devolve 400, não lista vazia.
    • unidadeId Restringe a um ponto de retirada ou entrega.
  • GET/pedidos/{id}pedidos.ler

    Um pedido pelo id.

  • GET/produtoscatalogo.ler

    Produtos com categoria e complementos.

    • categoriaId Só os de uma categoria.
    • disponivel true ou false.
  • GET/produtos/{id}catalogo.ler

    Um produto pelo id.

  • GET/categoriascatalogo.ler

    As categorias do catálogo.

  • GET/clientesclientes.ler

    Quem já comprou na loja, com total gasto e data do último pedido. Sem endereços salvos.

    • telefone Busca exata pelo número.
  • GET/assinaturasassinaturas.ler

    Assinantes do clube, com as cobranças mais recentes.

    • estado Filtra pelo estado da assinatura.
  • GET/assinaturas/{id}assinaturas.ler

    Uma assinatura pelo id.

  • GET/planosassinaturas.ler

    Os planos do clube.

    • ativo true ou false.
  • POST/cobrancascobrancas.criar

    Gera uma cobrança avulsa e devolve o link de pagamento.

  • POST/planosassinaturas.criar

    Cria um plano de assinatura. O link de adesão é /assinar/{id}.

  • GET/contaconta.ler

    Se a conta já pode cobrar e por qual meio, antes de gerar uma cobrança que morre.

Todas as listas aceitam limite e cursor.

Gerar uma cobrança avulsa

Só valorCents, descricao e o telefone de quem paga são obrigatórios. Sem pagador.cpf: quem paga se identifica na própria tela de pagamento, então a chave não precisa conhecer o documento de quem o parceiro não tem cadastrado.

curl -X POST https://www.junne.app/api/public/v1/cobrancas \
  -H "Authorization: Bearer $JUNNE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "valorCents": 15000,
    "descricao": "Consulta de retorno",
    "pagador": { "nome": "Maria Silva", "phone": "82991234567" },
    "referenciaExterna": "paciente_482"
  }'

# { "id": "ord_...", "shortCode": "A1B2C3", "url": "https://.../p/A1B2C3", "estado": "AGUARDANDO_PAGAMENTO" }

referenciaExterna é opaca: o Junne só guarda e devolve, nunca interpreta. Ela volta no pedido e no webhook, pra você conciliar com o registro da sua base. metodo (PIX ou CARD) é opcional — sem ele, quem paga escolhe na tela.

Criar um plano de assinatura

nome, valorCents e intervalo (WEEKLY, MONTHLY ou YEARLY) são obrigatórios. A resposta traz url, o link de adesão do plano — e é ali, na tela hospedada, que quem vai assinar se identifica. Não existe endpoint para assinar em nome de alguém: criar a assinatura por API seria assinar um cliente sem o consentimento dele.

curl -X POST https://www.junne.app/api/public/v1/planos \
  -H "Authorization: Bearer $JUNNE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "Acompanhamento mensal",
    "valorCents": 19900,
    "intervalo": "MONTHLY"
  }'

# { "id": "pl_...", "nome": "Acompanhamento mensal", "valorCents": 19900, "intervalo": "MONTHLY",
#   "url": "https://.../assinar/pl_...",
#   "referenciaExterna": { "parametro": "ref", "comoUsar": "https://.../assinar/pl_...?ref=SEU_ID_DO_ASSINANTE" } }

O link é por plano, não por pessoa. Para saber quem assinou, acrescente ?ref= ao link de cada assinante com o id dele na sua base: /assinar/pl_...?ref=paciente_482. O valor volta em referenciaExterna na assinatura e em todo webhook dela. Sem ele, o webhook de cobrança paga chega sem referência e só resta casar por telefone — que erra em número trocado, compartilhado e recadastro. Um ref fora do formato não derruba a adesão: ele é ignorado e a assinatura nasce sem referência.

Um pedido, por dentro

{
  "id": "ord_...",
  "codigo": "A1B2C3",
  "criadoEm": "2026-09-27T14:02:11.000Z",
  "atualizadoEm": "2026-09-27T14:31:44.000Z",
  "estado": "DELIVERED",
  "canal": "vitrine",
  "unidadeId": "unt_...",
  "cliente": { "nome": "Maria Silva", "telefone": "82991234567", "email": null },
  "itens": [
    {
      "nome": "Pizza grande",
      "quantidade": 1,
      "precoUnitarioCents": 5900,
      "adicionais": [{ "nome": "Borda recheada", "precoCents": 800 }]
    }
  ],
  "subtotalCents": 6700,
  "entregaCents": 700,
  "descontoCents": 0,
  "totalCents": 7400,
  "pagamento": { "tipo": "online", "metodo": "PIX", "pagoEm": "2026-09-27T14:03:50.000Z" },
  "entrega": {
    "endereco": "Rua das Flores, 100 — Centro",
    "lat": -9.66, "lng": -35.73,
    "distanciaMeters": 2300,
    "taxaCotadaCents": 700
  }
}

Identificadores internos da cobrança não saem na API. entrega é null em retirada e consumo no local.

Webhooks

Cadastre o endereço do seu servidor em Configurações → Integrações e escolha os eventos. O Junne faz POST em JSON a cada um.

pedido.criadopedido.pagopedido.aceitopedido.recusadopedido.saiu_para_entregapedido.entreguepedido.canceladopedido.expiradoassinatura.ativadaassinatura.cobranca_pagaassinatura.cobranca_falhouassinatura.cancelada

O corpo é sempre o mesmo envelope. Em evento de pedido, dados é o pedido no formato acima; em evento de assinatura, é a assinatura — os mesmos contratos da API, não um formato paralelo.

POST https://seu-servidor.com/junne
content-type: application/json
x-junne-signature: t=1774627331,v1=5a2f...

{
  "id": "ev_7c1f...",
  "tipo": "pedido.pago",
  "criadoEm": "2026-09-27T14:03:50.000Z",
  "dados": { ... }
}

Conferir a assinatura

O cabeçalho x-junne-signature traz o instante do envio e um HMAC-SHA256 do texto {t}.{corpo}, com o segredo do endpoint. Compare sobre o corpo cru, antes de qualquer parse: reserializar o JSON muda bytes e quebra a conferência.

const [t, v1] = cabecalho.split(',').map((p) => p.split('=')[1])

// Rejeite o que for velho: sem isso, quem capturar um corpo válido
// reenvia quando quiser. A janela do Junne é de 5 minutos.
if (Math.abs(Date.now() - Number(t) * 1000) > 5 * 60_000) return rejeitar()

const esperado = createHmac('sha256', SEGREDO).update(`${t}.${corpoCru}`).digest('hex')
if (!timingSafeEqual(Buffer.from(esperado, 'hex'), Buffer.from(v1, 'hex'))) return rejeitar()

Entrega e retentativa

Responda 2xx rápido e processe depois. Qualquer outra coisa — ou silêncio — vira retentativa, com esperas de 1, 5, 30, 120, 360 e 1440 minutos. Esgotadas as seis, a entrega é dada como falha definitiva e aparece no histórico do painel.

Redirecionamento não é seguido: o endereço que você cadastrar é o endereço que recebe.

Armadilhas

Cada uma destas já mordeu alguém — inclusive a gente. Elas não aparecem lendo a lista de campos.

  • pedido.pago não quer dizer “recebi online”. Ele também sai no pagamento na entrega, junto de pedido.entregue. Quem tratar “pago” como dinheiro que caiu na conta vai contar o mesmo pedido duas vezes. Olhe pagamento.tipo.
  • A ordem de chegada não é garantida. Dois eventos do mesmo instante podem chegar trocados. Ordene por criadoEm, nunca pela ordem em que seu servidor recebeu.
  • A entrega é “pelo menos uma vez”. O mesmo evento pode chegar repetido — uma resposta sua que demorou demais conta como falha e gera retentativa. Guarde o id do evento e ignore o que já viu.
  • assinatura.cancelada tem três nascedouros. Cancelamento pelo cliente, pelo lojista e por inadimplência chegam com o mesmo tipo. Se o seu fluxo trata os três diferente, olhe o estado da assinatura.
  • As cobranças da assinatura não são o histórico inteiro. O campo cobrancas traz só as mais recentes e não é paginado. Para o histórico completo, use /pedidos.
  • POST /cobrancas não tem chave de idempotência. Se a sua chamada estourar o tempo, a cobrança pode ter nascido mesmo assim — e repetir cria uma segunda, com dois links pagáveis para a mesma pessoa. Em timeout ou ERRO_INTERNO, não repita direto: peça GET /pedidos (vem do mais recente para o mais antigo) e procure a sua referenciaExterna nos últimos; só crie de novo se ela não estiver lá. É mais um motivo para mandar uma referenciaExterna em toda cobrança.
  • ate é exclusiva. ?de=2026-09-01&ate=2026-09-30 não traz o dia 30. Para fechar o mês, peça ate=2026-10-01.

Quando algo não bater

Escreva para suporte@junne.app com o codigo do erro, o horário e o endpoint. Se for sobre webhook, o id do evento encontra a entrega no nosso lado.