Nova
API v1 · Estável

API Nova para Desenvolvedores

Receba PIX, gere links de pagamento e ouça webhooks a partir do seu próprio app. A API é previsível: chave Bearer no cabeçalho, JSON na entrada, JSON na resposta.

Nesta página
  1. Contrato da API
  2. Início rápido
  3. Pacote SDK
  4. Autenticação
  5. Chamadas do navegador (CORS)
  6. Limites de taxa
  7. Idempotência
  8. Erros
  9. Endpoints
    1. Sistema
    2. GET Verificação de vida (liveness)
    3. GET Round-trip autenticado
    4. Cobranças PIX
    5. POST Criar uma cobrança PIX (QR code)
    6. GET Consultar uma cobrança PIX
    7. Links de pagamento
    8. POST Criar um link de pagamento reutilizável
    9. GET Consultar um link de pagamento
    10. DELETE Pausar um link de pagamento
    11. Webhooks
    12. GET Listar webhooks
    13. POST Assinar eventos
    14. DELETE Cancelar a assinatura
    15. POST Disparar uma entrega de teste
  10. Webhooks
    1. Webhooks: eventos
    2. Webhooks: verificar a assinatura
    3. Webhooks: política de retry
  11. Versionamento
  12. Bug bounty
  13. Suporte
  14. llms.md

Contrato de produção da API #

A v1 da Nova é pequena de propósito. Integre seguindo estas regras:

Quebra mínima: a v1 mantém o formato atual das respostas. Adicionamos campos, cabeçalhos e endpoints sem mudar nomes ou sentidos existentes.

Acesso à API #

Você precisa de três coisas:

  1. Uma conta Nova. Crie uma se ainda não tiver.
  2. Aprovação para usar a API. Abra o aplicativo Nova, vá em Conta → Desenvolvedor, preencha o pedido e aguarde aprovação (geralmente em menos de um dia útil).
  3. Uma chave de API. Após aprovação, a mesma tela permite criar uma. Copie o segredo na hora — guardamos só o hash e nunca mostramos de novo.

Teste que está tudo conectado:

curl -H "Authorization: Bearer $NOVA_API_KEY" https://novadao.app/api/v1/ping

Você deve receber um JSON com o id da sua chave, o tier e o horário do servidor. Se vier 401, confira a chave. Se vier 403 falando de domínio, veja Chamadas do navegador (CORS).

Gere a sua primeira cobrança PIX de R$ 1 (use um endereço Liquid de verdade que você controla):

curl -X POST https://novadao.app/api/v1/pix/charges \
  -H "Authorization: Bearer $NOVA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_in_cents": 100,
    "depix_address": "lq1..."
  }'

Você vai receber um id, uma string qr_copy_paste (cole em qualquer app de banco brasileiro) e uma qr_image_url com a marca da Nova. Pague o QR para ver a cobrança virar paid.

Pacote SDK #

O pacote TypeScript @novadao/sdk é fino: um transporte HTTP, grupos tipados por recurso, erros tipados, idempotência como opção explícita e verificação de webhook incluída. Ele não esconde retries de POST se o chamador não enviou uma chave de idempotência.

Instale pelo npm:

npm install @novadao/sdk

Página do pacote: @novadao/sdk no npm.

type NovaApiError = {
  status: number;
  code: string;
  message: string;
  requestId: string;
};

type PixCharge = {
  id: string;
  status: 'pending' | 'paid' | 'expired' | 'failed';
  amountInCents: number | null;
  qrCopyPaste: string | null;
  qrImageUrl: string | null;
  blockchainTxId: string | null;
  createdAt: string;
};

type PaymentLink = {
  id: string;
  name: string;
  mode: 'fixed' | 'range' | 'open';
  status: 'active' | 'paused';
  url: string;
  handle: string;
  slug: string;
  amountBrl: number | null;
  minBrl: number | null;
  maxBrl: number | null;
  depixAddress: string;
  createdAt: string;
};

type WebhookEndpoint = {
  id: number;
  url: string;
  events: string[];
  status: 'active' | 'disabled';
  createdAt: string;
};

type CreatedWebhookEndpoint = WebhookEndpoint & {
  signingSecret: string;
};

type NovaClient = {
  pix: {
    createCharge(
      input: { amountInCents: number; depixAddress: string },
      options: { idempotencyKey: string },
    ): Promise<PixCharge>;
    getCharge(id: string): Promise<PixCharge>;
  };
  paymentLinks: {
    create(input: CreatePaymentLink, options?: { idempotencyKey?: string }): Promise<PaymentLink>;
    get(id: string): Promise<PaymentLink>;
    pause(id: string): Promise<PaymentLink>;
  };
  webhooks: {
    list(): Promise<WebhookEndpoint[]>;
    create(
      input: CreateWebhookEndpoint,
      options: { idempotencyKey: string },
    ): Promise<CreatedWebhookEndpoint>;
    remove(id: number): Promise<{ id: number; deleted: true }>;
    test(id: number): Promise<{ deliveryId: string; queuedAt: string }>;
    verifySignature(
      rawBody: string | Uint8Array,
      header: string,
      secret: string,
      options?: { toleranceSeconds?: number },
    ): Promise<boolean>;
  };
};

Uso recomendado:

import { NovaClient, createIdempotencyKey } from '@novadao/sdk';

const nova = new NovaClient({
  apiKey: process.env.NOVA_API_KEY!,
});

const charge = await nova.pix.createCharge(
  { amountInCents: 100, depixAddress: 'lq1...' },
  { idempotencyKey: createIdempotencyKey('pix') },
);

Regras de implementação:

Autenticação #

Uma chave de API é uma sequência secreta que prova que o pedido veio de você. Trate como senha: nunca coloque em código que roda no navegador, nunca dê commit no GitHub, e troque se desconfiar que vazou.

Envie sua chave no cabeçalho Bearer padrão:

Authorization: Bearer nv_live_8aXk3rT9...
Um cabeçalho só. A Nova não aceita X-Api-Key. Enviar isso retorna 401 auth_use_bearer apontando para cá.

O que significa “aprovação”?

A API faz coisas reais — gera cobranças PIX, cria links de pagamento — então aprovamos cada conta antes que qualquer chave possa ser criada. O formulário no aplicativo pergunta o que você está construindo e qual volume espera. A maioria dos pedidos é revisada em até um dia útil.

Lista de IPs permitidos

Se você cadastrar uma lista de IPs na sua conta de desenvolvedor (também em Conta → Desenvolvedor), a API rejeita qualquer requisição cujo IP de origem não esteja na lista. É opcional, mas muito recomendado para servidores em produção.

Chamadas do navegador (CORS) #

Por padrão a API da Nova aceita apenas chamadas servidor-a-servidor. Se você tentar chamar /api/v1/* de um navegador, a resposta é 403 browser_origin_disallowed.

Isso é intencional. Uma chave de API no navegador é uma chave de API no devtools de cada visitante. Se você realmente precisa chamar do navegador (por exemplo, uma integração híbrida onde a mesma chave assina chamadas servidor e cliente), pode cadastrar domínios permitidos específicos para essa chave. O painel mostra um aviso bem claro quando você faz isso.

Recomendado: mantenha o uso da chave no seu backend. Use um proxy fininho na sua própria infraestrutura se precisar expor algo ao navegador.

Limites de taxa #

Dois orçamentos, ambos por chave de API:

Endpoints específicos têm sublimites próprios — veja as observações de cada endpoint abaixo. Toda resposta autenticada traz:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1714000060
X-RateLimit-Daily-Limit: 10000
X-RateLimit-Daily-Remaining: 9842
X-RateLimit-Daily-Reset: 1714086400

No 429 rate_limited você também recebe Retry-After (em segundos). Espere e tente de novo — não fique em loop.

Idempotência #

Falhas de rede acontecem. Se a sua requisição POST /pix/charges der timeout, você não sabe se a cobrança foi criada ou não. Chaves de idempotência tornam a re-tentativa segura.

Envie um cabeçalho Idempotency-Key único (1..255 caracteres ASCII, qualquer coisa que você gerar) em todo POST que possa ser repetido. Se a mesma chave chegar duas vezes com o mesmo corpo, devolvemos a resposta cacheada com X-Idempotent-Replay: true em vez de criar uma segunda cobrança. Se a mesma chave chegar com um corpo diferente, devolvemos 409 idempotency_key_reused para você saber que algo está estranho.

Respostas cacheadas vivem por 24 horas. Erros do servidor (5xx) nunca são cacheados, então você sempre pode repetir uma falha transitória com a mesma chave.

Erros #

Todos os erros compartilham o mesmo envelope:

{
  "error": {
    "code": "rate_limited",
    "message": "You are sending requests too quickly. Slow down and try again.",
    "request_id": "req_8aXk3rT9...",
    "docs_url": "https://novadao.app/devs/errors#rate_limited"
  }
}

O request_id é único por requisição. Inclua no chamado de suporte — assim achamos o log do servidor na hora.

HTTP Código Significado
401 auth_missing Nenhum cabeçalho Authorization foi enviado.
401 auth_use_bearer Você enviou X-Api-Key. Use Authorization: Bearer <chave> no lugar.
401 auth_invalid A chave de API é inválida, revogada ou não aprovada.
403 browser_origin_disallowed Requisição do navegador cujo Origin não está em allowed_domains da chave. Chamadas servidor-a-servidor não devem enviar Origin.
403 ip_not_allowlisted O IP da requisição não está na lista de IPs permitidos da conta.
429 rate_limited Muitas requisições. Leia X-RateLimit-Reset e Retry-After. O esgotamento do limite diário usa o mesmo código.
400 / 409 / 422 invalid_payload O corpo, caminho ou operação falhou na validação. SDKs devem guardar o status HTTP e o error.code.
400 invalid_json O corpo da requisição está vazio ou não é um JSON válido.
413 payload_too_large O corpo da requisição é maior do que o endpoint aceita.
400 idempotency_key_required POST /pix/charges exige Idempotency-Key em toda requisição.
409 idempotency_key_in_progress Uma requisição com esta Idempotency-Key ainda está processando. Repita a mesma requisição em instantes.
409 idempotency_key_reused Você enviou a mesma Idempotency-Key com um corpo diferente. Gere uma nova chave para novas requisições.
404 not_found O recurso não existe ou não é seu.
503 payment_service_unavailable O provedor de pagamento está temporariamente indisponível. Repita a mesma requisição lógica com a mesma Idempotency-Key.
500 internal_error Algo deu errado do nosso lado.

Endpoints #

URL base: https://novadao.app. Toda rota autenticada está sob /api/v1/.

GET /api/v1/health Público

Verificação de vida (liveness).

Sem autenticação, sem limite de taxa, sem banco. Retorna 200 enquanto a API estiver rodando. Use em páginas de status e monitores de uptime.

Formato da resposta

{ "ok": true, "service": "nova-public-api", "server_time": "ISO8601" }

Conferir se a API está no ar

cURL
cURL
curl https://novadao.app/api/v1/health
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/health');
const data = await res.json();
console.log(data);
Python
Python
import httpx

res = httpx.get('https://novadao.app/api/v1/health')
print(res.json())

Retorna

JSON
{
  "ok": true,
  "service": "nova-public-api",
  "server_time": "2026-04-25T12:34:56.000Z"
}
GET /api/v1/ping Auth Bearer

Round-trip autenticado.

Confirma que sua chave de API funciona e mostra o que o servidor vê: o id da chave, o tier, os domínios permitidos e o horário do servidor. O botão "testar conexão" do painel chama este endpoint.

Parâmetros

  • Authorization obrigatório header · string

    Bearer <sua-chave-de-api>

Formato da resposta

{ "ok": true, "key_id": number, "tier": "tier1" | "tier2", "allowed_domains": string[], "server_time": "ISO8601", "request_id": "req_..." }

Validar sua chave

cURL
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/ping
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/ping', {
  headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
console.log(await res.json());
Python
Python
import os, httpx

res = httpx.get(
    'https://novadao.app/api/v1/ping',
    headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
print(res.json())

Retorna

JSON
{
  "ok": true,
  "key_id": 42,
  "tier": "tier1",
  "allowed_domains": [],
  "server_time": "2026-04-25T12:34:56.000Z",
  "request_id": "req_8aXk..."
}
POST /api/v1/pix/charges Auth Bearer Idempotency-Key obrigatória

Criar uma cobrança PIX (QR code).

Gera um QR code PIX que o seu cliente pode pagar. Preste atenção a dois cabeçalhos: Authorization e Idempotency-Key. A Idempotency-Key é OBRIGATÓRIA — ela permite repetir uma chamada após erro de rede sem cobrar duas vezes.

Limite de taxa: tier1: 30/min · tier2: 300/min

Parâmetros

  • Authorization obrigatório header · string

    Bearer <sua-chave-de-api>

  • Idempotency-Key obrigatório header · string

    Um valor único que você gera por requisição (1..255 caracteres ASCII). `X-Idempotency-Key` também é aceito.

  • amount_in_cents obrigatório body · integer

    Valor da cobrança em centavos (BRL). Mínimo 100 (R$ 1,00).

  • depix_address obrigatório body · string

    Endereço confidencial da rede Liquid (`lq1...`) que vai receber os DePix quando a cobrança liquidar.

Formato da resposta

{ "id": "...", "status": "pending", "amount_in_cents": number, "qr_copy_paste": "...", "qr_image_url": "...", "blockchain_tx_id": null, "created_at": "ISO8601" }

Criar uma cobrança de R$ 50

cURL
cURL
curl -X POST https://novadao.app/api/v1/pix/charges \
  -H "Authorization: Bearer $NOVA_API_KEY" \
  -H "Idempotency-Key: order-1234" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_in_cents": 5000,
    "depix_address": "lq1..."
  }'
Node.js
Node.js
import { randomUUID } from 'node:crypto';

const res = await fetch('https://novadao.app/api/v1/pix/charges', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
    'Idempotency-Key': randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount_in_cents: 5000,
    depix_address: 'lq1...',
  }),
});
const charge = await res.json();
console.log(charge.id, charge.qr_copy_paste);
Python
Python
import os, uuid, httpx

res = httpx.post(
    'https://novadao.app/api/v1/pix/charges',
    headers={
        'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}',
        'Idempotency-Key': str(uuid.uuid4()),
    },
    json={
        'amount_in_cents': 5000,
        'depix_address': 'lq1...',
    },
)
charge = res.json()
print(charge['id'], charge['qr_copy_paste'])

Retorna

JSON
{
  "id": "ch_a8b3...",
  "status": "pending",
  "amount_in_cents": 5000,
  "qr_copy_paste": "00020126580014BR.GOV.BCB.PIX...",
  "qr_image_url": "https://novadao.app/api/v1/pix/charges/ch_a8b3.../qr.png",
  "blockchain_tx_id": null,
  "created_at": "2026-04-25T12:34:56.000Z"
}
GET /api/v1/pix/charges/:id Auth Bearer

Consultar uma cobrança PIX.

Retorna o status atual de uma cobrança PIX. Assine `pix.charge.received` quando precisar da confirmação bancária do pagamento, e `pix.charge.paid` quando precisar da transação liquidada na Liquid.

Parâmetros

  • id obrigatório path · string

    O id da cobrança retornado por POST /pix/charges.

Formato da resposta

{ "id": "...", "status": "pending" | "paid" | "expired" | "failed", "amount_in_cents": number, "qr_copy_paste": "...", "qr_image_url": "...", "blockchain_tx_id": "..." | null, "created_at": "ISO8601" }

Conferir se a cobrança foi paga

cURL
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/pix/charges/ch_a8b3
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/pix/charges/ch_a8b3', {
  headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const charge = await res.json();
console.log(charge.status);
Python
Python
import os, httpx

res = httpx.get(
    'https://novadao.app/api/v1/pix/charges/ch_a8b3',
    headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
charge = res.json()
print(charge['status'])

Retorna

JSON
{
  "id": "ch_a8b3...",
  "status": "paid",
  "amount_in_cents": 5000,
  "qr_copy_paste": "00020126580014BR.GOV.BCB.PIX...",
  "qr_image_url": "https://novadao.app/api/v1/pix/charges/ch_a8b3.../qr.png",
  "blockchain_tx_id": "7f4a...",
  "created_at": "2026-04-25T12:34:56.000Z"
}
GET /api/v1/webhooks Auth Bearer

Listar webhooks.

Retorna todos os webhooks da sua conta.

Formato da resposta

{ "webhooks": [{ "id": number, "url": "...", "events": string[], "status": "active" | "disabled", "created_at": "ISO8601" }] }

Listar webhooks configurados

cURL
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \
  https://novadao.app/api/v1/webhooks
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks', {
  headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const data = await res.json();
console.log(data.webhooks.length);
Python
Python
import os, httpx

res = httpx.get(
    'https://novadao.app/api/v1/webhooks',
    headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
data = res.json()
print(len(data['webhooks']))

Retorna

JSON
{
  "webhooks": [
    {
      "id": 7,
      "url": "https://yourshop.com/webhooks/nova",
      "events": ["pix.charge.paid", "payment_link.paid"],
      "status": "active",
      "created_at": "2026-04-25T12:34:56.000Z"
    }
  ]
}
POST /api/v1/webhooks Auth Bearer Idempotency-Key suportada

Assinar eventos.

Cria um inscrito de webhook. A resposta inclui um `signing_secret` mostrado uma única vez — guarde com cuidado; você vai precisar dele para verificar a assinatura de cada entrega.

Parâmetros

  • url obrigatório body · string (URL)

    Endpoint HTTPS que vai receber as entregas via POST.

  • events obrigatório body · string[]

    Um ou mais entre: pix.charge.created, pix.charge.received, pix.charge.paid, pix.charge.expired, pix.charge.failed, payment_link.paid.

Formato da resposta

{ "id": number, "url": "...", "events": string[], "status": "active", "signing_secret": "<base64url, shown once>", "created_at": "ISO8601" }

Assinar cobranças pagas

cURL
cURL
curl -X POST https://novadao.app/api/v1/webhooks \
  -H "Authorization: Bearer $NOVA_API_KEY" \
  -H "Idempotency-Key: webhook-paid-events-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourshop.com/webhooks/nova",
    "events": ["pix.charge.paid", "payment_link.paid"]
  }'
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
    'Idempotency-Key': 'webhook-paid-events-v1',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://yourshop.com/webhooks/nova',
    events: ['pix.charge.paid', 'payment_link.paid'],
  }),
});
const wh = await res.json();
console.log('Save this signing secret:', wh.signing_secret);
Python
Python
import os, httpx

res = httpx.post(
    'https://novadao.app/api/v1/webhooks',
    headers={
        'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}',
        'Idempotency-Key': 'webhook-paid-events-v1',
    },
    json={
        'url': 'https://yourshop.com/webhooks/nova',
        'events': ['pix.charge.paid', 'payment_link.paid'],
    },
)
wh = res.json()
print('Save this signing secret:', wh['signing_secret'])

Retorna

JSON
{
  "id": 7,
  "url": "https://yourshop.com/webhooks/nova",
  "events": ["pix.charge.paid", "payment_link.paid"],
  "status": "active",
  "signing_secret": "Rll0hXsdn2hzp1G6...",
  "created_at": "2026-04-25T12:34:56.000Z"
}
DELETE /api/v1/webhooks/:id Auth Bearer

Cancelar a assinatura.

Remove um inscrito de webhook. Entregas já enfileiradas ainda são tentadas.

Parâmetros

  • id obrigatório path · integer

    Id do webhook.

Formato da resposta

{ "id": number, "deleted": true }

Remover um webhook

cURL
cURL
curl -X DELETE https://novadao.app/api/v1/webhooks/7 \
  -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks/7', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
console.log(await res.json());
Python
Python
import os, httpx

res = httpx.delete(
    'https://novadao.app/api/v1/webhooks/7',
    headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
print(res.json())

Retorna

JSON
{
  "id": 7,
  "deleted": true
}
POST /api/v1/webhooks/:id/test Auth Bearer

Disparar uma entrega de teste.

Enfileira uma entrega `webhook.test.ping` para a URL do seu webhook. Use para conferir que seu endpoint recebe e verifica assinaturas corretamente. A entrega é assinada igual a um evento real.

Parâmetros

  • id obrigatório path · integer

    Id do webhook.

Formato da resposta

{ "delivery_id": "whd_...", "queued_at": "ISO8601" }

Enviar um evento assinado de teste

cURL
cURL
curl -X POST https://novadao.app/api/v1/webhooks/7/test \
  -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
Node.js
const res = await fetch('https://novadao.app/api/v1/webhooks/7/test', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const delivery = await res.json();
console.log(delivery.delivery_id);
Python
Python
import os, httpx

res = httpx.post(
    'https://novadao.app/api/v1/webhooks/7/test',
    headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
delivery = res.json()
print(delivery['delivery_id'])

Retorna

JSON
{
  "delivery_id": "whd_abc123",
  "queued_at": "2026-04-25T12:34:56.000Z"
}

Webhooks: eventos #

A Nova envia um payload JSON para a URL do seu webhook sempre que algo acontece com uma das suas cobranças ou links de pagamento. Assine os eventos que importam quando você criar o webhook.

Para cobranças PIX D+2, use pix.charge.received para confirmar que o pagador concluiu o PIX no trilho bancário. Um pagamento delayed chega com status: "delayed", settlement_status: "delayed", delay_until, confirmed_at e blockchain_tx_id: null. Use pix.charge.paid apenas para a liquidação final na Liquid com blockchain_tx_id, e continue ouvindo pix.charge.failed para cobranças estornadas, canceladas ou com erro.

Toda entrega inclui estes cabeçalhos:

Nova-Signature: t=1714000000,v1=8a7c3b...
Nova-Event: pix.charge.paid
Nova-Delivery-Id: whd_abc123
Nova-Api-Version: 2026-04-24

Webhooks: verificar a assinatura #

A gente manda um carimbo; você confere se o carimbo bate antes de confiar na mensagem. Sem verificação, qualquer um que descobrir sua URL pode mandar eventos falsos. O cabeçalho Nova-Signature tem duas partes: t (timestamp Unix em segundos) e v1 (HMAC-SHA256 em hex). A string assinada é t + "." + raw_body. Calcule o HMAC com o seu signing_secret (devolvido uma única vez na criação do webhook) e compare.

Node.js
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyNovaWebhook(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  if (expected.length !== v1.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Python
Python
import hmac, hashlib, time

def verify_nova_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    try:
        t = int(parts['t']); v1 = parts['v1']
    except KeyError:
        return False
    if abs(int(time.time()) - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)
PHP
PHP
function verify_nova_webhook(string $body, string $header, string $secret, int $tolerance = 300): bool {
    parse_str(str_replace(',', '&', $header), $parts);
    $t = (int) ($parts['t'] ?? 0); $v1 = $parts['v1'] ?? '';
    if (!$t || !$v1) return false;
    if (abs(time() - $t) > $tolerance) return false;
    $expected = hash_hmac('sha256', $t . '.' . $body, $secret);
    return hash_equals($expected, $v1);
}
Use o corpo bruto. Se o seu framework parsear o JSON antes de você conseguir hashear, a assinatura não vai bater — Express, Fastify e Bun expõem os bytes brutos via configuração ou middleware. A maioria dos chamados que recebemos no suporte é por causa disso.

Webhooks: política de retry #

Se o seu endpoint responder 2xx em 15 segundos, a entrega é marcada como concluída. Qualquer outra resposta é falha. Falhas tentam de novo seis vezes com espera crescente:

Depois da última tentativa, a entrega é marcada como failed. O painel mostra ela em "Entregas de webhook" com um botão de replay manual. O aplicativo Nova também coloca uma notificação na sua caixa.

1m 5m 30m 2h 6h 24h

Versionamento #

A versão atual é /api/v1/. Tratamos como contrato permanente. Campos e tipos de evento novos são aditivos e não exigem que você mude nada. Se algum dia rolar uma mudança que quebra, ela aparece em /api/v2/ e a v1 fica viva por pelo menos 12 meses. Durante a janela de sunset, toda resposta da v1 carrega o cabeçalho Deprecation: true e uma data Sunset.

Todo payload de webhook também inclui um campo api_version para que assinantes possam ramificar o código sem fixar datas.

Bug bounty e divulgação responsável #

A Nova recebe relatos de segurança sobre superfícies do produto e da API. Envie achados em privado para hello@novadao.app com o assunto [security] <descrição curta>. Não abra issues públicas nem publique detalhes até a Nova confirmar que a divulgação está liberada.

Escopo

Regras

Fora de escopo

Recompensas

Relatos validados podem ser elegíveis a recompensas discricionárias. A decisão considera severidade, impacto real, reprodutibilidade, explorabilidade, qualidade do relato e se o problema já era conhecido.

Suporte

Duas formas de falar com a gente:

  • Bot de suporte no Telegram — caminho mais rápido, com resposta em até uma hora em horário comercial.
  • Inclua o request_id do envelope de erro quando escrever. Conseguimos rastrear cada chamada na hora com ele.

Para integrações com LLMs, aponte sua ferramenta para /pt/devs/llms.md — mesmo conteúdo, em formato legível por máquina.