API Nova para Desarrolladores
Recibe PIX, genera enlaces de pago y escucha webhooks desde tu propia app. La API es predecible: clave Bearer en el encabezado, JSON de entrada, JSON de respuesta.
En esta página
- Contrato de la API
- Inicio rápido
- Paquete SDK
- Autenticación
- Llamadas desde navegador (CORS)
- Límites de tasa
- Idempotencia
- Errores
- Endpoints
- Sistema
- GET Sonda de disponibilidad (liveness)
- GET Round-trip autenticado
- Cobros PIX
- POST Crear un cobro PIX (código QR)
- GET Consultar un cobro PIX
- Enlaces de pago
- POST Crear un enlace de pago reutilizable
- GET Consultar un enlace de pago
- DELETE Pausar un enlace de pago
- Webhooks
- GET Listar webhooks
- POST Suscribirse a eventos
- DELETE Cancelar la suscripción
- POST Disparar una entrega de prueba
- Webhooks
- Versionado
- Bug bounty
- Soporte
- llms.md
Contrato de producción de la API #
La v1 de Nova es pequeña a propósito. Integra siguiendo estas reglas:
- Una ruta base: toda ruta autenticada vive bajo
/api/v1. - Un tipo de auth: envía
Authorization: Bearer <clave>. No envíes claves de API desde código de navegador. - Una superficie de SDK: los SDKs públicos deben llamar solo a
/api/v1/*./api/wallet/*es para la SPA Nova con sesión y usa cookies con CSRF. - Respuesta directa en éxito: las respuestas exitosas devuelven el recurso. Los errores siempre usan el mismo envelope
{ "error": ... }. - Reintentos seguros: los POSTs que mueven dinero requieren
Idempotency-Key. También se aceptaX-Idempotency-Key. - Webhooks firmados: verifica
Nova-Signaturecon el cuerpo crudo antes de confiar en un evento.
Acceso a la API #
Necesitas tres cosas:
- Una cuenta Nova. Crea una si todavía no tienes.
- Aprobación para usar la API. Abre la app Nova, ve a Cuenta → Desarrollador, llena la solicitud y espera la aprobación (normalmente menos de un día hábil).
- Una clave de API. Tras la aprobación, la misma pantalla te deja crear una. Copia el secreto al instante — solo guardamos el hash y nunca lo volvemos a mostrar.
Comprueba que todo está conectado:
curl -H "Authorization: Bearer $NOVA_API_KEY" https://novadao.app/api/v1/ping
Deberías recibir un JSON con el id de tu clave, el tier y la hora del servidor. Si te llega 401, revisa la clave. Si te llega 403 hablando de un dominio, ve a Llamadas desde navegador (CORS).
Genera tu primer cobro PIX de R$ 1 (usa una dirección Liquid real que controles):
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..."
}'
Vas a recibir un id, una cadena qr_copy_paste (pégala en cualquier app bancaria brasileña) y una qr_image_url con la marca de Nova. Paga el QR y verás el cobro pasar a paid.
Paquete SDK #
El paquete TypeScript @novadao/sdk es delgado: un transporte HTTP, grupos tipados por recurso, errores tipados, idempotencia como opción explícita y verificación de webhooks incluida. No oculta reintentos de POST si quien llama no mandó una clave de idempotencia.
Instala desde npm:
npm install @novadao/sdk
Página del paquete: @novadao/sdk en 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') },
);
Reglas de implementación:
- Lanza un
NovaApiErrorconstatus,code,messageyrequestIdcuando la API devuelva un envelope de error. - Tipa fallas usando
statusycode. El mismo código puede aparecer con distinto status HTTP. - Reintenta solo errores de red,
408,429y5xx. RespetaRetry-After. - Nunca reintentes
POST /pix/chargessin clave de idempotencia. Para la misma solicitud lógica, reintenta con la misma clave; usa una clave nueva solo para un cobro nuevo. - Exige clave de idempotencia en
webhooks.createen el SDK, aunque la API cruda la acepte como opcional. - Acepta snake_case de la API, pero expón tipos camelCase en TypeScript.
Autenticación #
Una clave de API es una cadena secreta que demuestra que la solicitud viene de ti. Trátala como una contraseña: nunca la pongas en código que corre en el navegador, nunca le hagas commit en GitHub, y cámbiala si sospechas que se filtró.
Envía tu clave en el encabezado Bearer estándar:
Authorization: Bearer nv_live_8aXk3rT9...
X-Api-Key. Enviarlo devuelve 401 auth_use_bearer apuntando aquí.¿Qué significa “aprobación”?
La API hace cosas reales — genera cobros PIX, crea enlaces de pago — así que aprobamos a mano cada cuenta antes de que pueda crear claves. El formulario en la app pregunta qué estás construyendo y cuánto volumen esperas. La mayoría se revisa en un día hábil.
Lista de IPs permitidos
Si configuras una lista de IPs en tu cuenta de desarrollador (también en Cuenta → Desarrollador), la API rechaza cualquier solicitud cuyo IP de origen no esté en la lista. Es opcional, pero muy recomendado para servidores en producción.
Llamadas desde navegador (CORS) #
Por defecto, la API de Nova solo acepta llamadas servidor-a-servidor. Si intentas llamar a /api/v1/* desde un navegador, la respuesta es 403 browser_origin_disallowed.
Es intencional. Una clave de API en el navegador es una clave de API en las devtools de cada visitante. Si necesitas absolutamente llamar desde un navegador (por ejemplo, una integración híbrida donde la misma clave firma llamadas servidor y cliente), puedes registrar dominios permitidos específicos para esa clave. El panel muestra una advertencia clara cuando lo haces.
Límites de tasa #
Dos presupuestos, ambos por clave de API:
- Por minuto — 60 (tier1, por defecto) o 600 (tier2, bajo demanda).
- Por día — 10.000 (tier1) o 200.000 (tier2).
Algunos endpoints tienen sub-límites propios — revisa las notas de cada uno abajo. Toda respuesta autenticada incluye:
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
En 429 rate_limited también recibes Retry-After (en segundos). Espera y reintenta — no entres en bucle.
Idempotencia #
Los hipos de red existen. Si tu solicitud POST /pix/charges se queda en timeout, no sabes si el cobro se creó o no. Las claves de idempotencia hacen que reintentar sea seguro.
Envía un encabezado Idempotency-Key único (1..255 caracteres ASCII, lo que generes) en cada POST reintentable. Si la misma clave llega dos veces con el mismo cuerpo, devolvemos la respuesta cacheada con X-Idempotent-Replay: true en vez de crear un segundo cobro. Si la misma clave llega con un cuerpo distinto, devolvemos 409 idempotency_key_reused para que sepas que algo está raro.
Las respuestas cacheadas viven 24 horas. Los errores del servidor (5xx) nunca se cachean, así que siempre puedes reintentar una falla transitoria con la misma clave.
- Obligatorio en
POST /pix/charges(dinero real). - Opcional en
POST /payment-linksyPOST /webhooks. Manda uno si quieres reintentos seguros.
Errores #
Todos los errores comparten el mismo 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"
}
}
El request_id es único por solicitud. Inclúyelo en cualquier ticket de soporte — nos permite encontrar el log del servidor al instante.
| HTTP | Código | Significado |
|---|---|---|
| 401 | auth_missing | No se envió el encabezado Authorization. |
| 401 | auth_use_bearer | Enviaste X-Api-Key. Usa Authorization: Bearer <clave> en su lugar. |
| 401 | auth_invalid | La clave de API es inválida, revocada o no aprobada. |
| 403 | browser_origin_disallowed | Solicitud del navegador cuyo Origin no está en allowed_domains de la clave. Las llamadas servidor-a-servidor no deben enviar Origin. |
| 403 | ip_not_allowlisted | El IP de la solicitud no está en la lista de IPs permitidos de la cuenta. |
| 429 | rate_limited | Demasiadas solicitudes. Lee X-RateLimit-Reset y Retry-After. El agotamiento del límite diario usa el mismo código. |
| 400 / 409 / 422 | invalid_payload | El cuerpo, ruta u operación falló la validación. Los SDKs deben guardar el status HTTP y error.code. |
| 400 | invalid_json | El cuerpo de la solicitud está vacío o no es JSON válido. |
| 413 | payload_too_large | El cuerpo de la solicitud es mayor de lo que acepta el endpoint. |
| 400 | idempotency_key_required | POST /pix/charges requiere Idempotency-Key en cada solicitud. |
| 409 | idempotency_key_in_progress | Una solicitud con esta Idempotency-Key aún se está procesando. Reintenta la misma solicitud en breve. |
| 409 | idempotency_key_reused | Enviaste la misma Idempotency-Key con un cuerpo distinto. Genera una nueva clave para nuevas solicitudes. |
| 404 | not_found | El recurso no existe o no es tuyo. |
| 503 | payment_service_unavailable | El proveedor de pago está temporalmente fuera de servicio. Reintenta la misma solicitud lógica con la misma Idempotency-Key. |
| 500 | internal_error | Algo salió mal de nuestro lado. |
Endpoints #
URL base: https://novadao.app. Toda ruta autenticada está bajo /api/v1/.
Sonda de disponibilidad (liveness).
Sin autenticación, sin límite de tasa, sin base de datos. Devuelve 200 mientras la API esté corriendo. Úsala en páginas de status y monitores de uptime.
Formato de la respuesta
Comprobar que la API está activa
cURL
curl https://novadao.app/api/v1/health
Node.js
const res = await fetch('https://novadao.app/api/v1/health');
const data = await res.json();
console.log(data); Python
import httpx
res = httpx.get('https://novadao.app/api/v1/health')
print(res.json()) Devuelve
{
"ok": true,
"service": "nova-public-api",
"server_time": "2026-04-25T12:34:56.000Z"
} Round-trip autenticado.
Confirma que tu clave de API funciona y muestra lo que ve el servidor: el id de la clave, el tier, los dominios permitidos y la hora del servidor. El botón "probar conexión" del panel llama este endpoint.
Parámetros
- Authorization obligatorio
Bearer <tu-clave-de-api>
Formato de la respuesta
Validar tu clave
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/ping
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
import os, httpx
res = httpx.get(
'https://novadao.app/api/v1/ping',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
print(res.json()) Devuelve
{
"ok": true,
"key_id": 42,
"tier": "tier1",
"allowed_domains": [],
"server_time": "2026-04-25T12:34:56.000Z",
"request_id": "req_8aXk..."
} Crear un cobro PIX (código QR).
Genera un código QR PIX que tu cliente puede pagar. Presta atención a dos encabezados: Authorization e Idempotency-Key. La Idempotency-Key es OBLIGATORIA — te deja reintentar tras un error de red sin cobrar dos veces.
Límite de tasa: tier1: 30/min · tier2: 300/min
Parámetros
- Authorization obligatorio
Bearer <tu-clave-de-api>
- Idempotency-Key obligatorio
Un valor único que generas por solicitud (1..255 caracteres ASCII). También se acepta `X-Idempotency-Key`.
- amount_in_cents obligatorio
Monto del cobro en centavos (BRL). Mínimo 100 (R$ 1,00).
- depix_address obligatorio
Dirección confidencial de la red Liquid (`lq1...`) que recibirá los DePix cuando el cobro liquide.
Formato de la respuesta
Crear un cobro de R$ 50
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
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
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']) Devuelve
{
"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"
} Consultar un cobro PIX.
Devuelve el estado actual de un cobro PIX. Suscríbete a `pix.charge.received` cuando necesites la confirmación bancaria del pago, y a `pix.charge.paid` cuando necesites la transacción liquidada en Liquid.
Parámetros
- id obligatorio
El id del cobro devuelto por POST /pix/charges.
Formato de la respuesta
Comprobar si el cobro fue pagado
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/pix/charges/ch_a8b3
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
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']) Devuelve
{
"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"
} Crear un enlace de pago reutilizable.
Genera una URL pública de checkout (`https://novadao.app/c/<handle>/<slug>`) que acepta pagos. Tres modos: `fixed` (un único monto), `range` (mín/máx), `open` (cualquier monto, con límites opcionales).
Límite de tasa: tier1: 60/min
Parámetros
- name obligatorio
1 a 80 caracteres. Se usa para generar el slug.
- mode obligatorio
Modelo de precio.
- amount_brl
Obligatorio cuando mode es "fixed".
- min_brl
Se usa cuando mode es "range" (obligatorio) u "open" (opcional).
- max_brl
Se usa cuando mode es "range" (obligatorio) u "open" (opcional).
- depix_address obligatorio
Dirección confidencial de la red Liquid (`lq1...`) que recibe DePix en cada pago exitoso.
- options
Opcional: { ask_name?: bool, ask_email?: bool, thank_you_message?: string, sales_limit?: int }.
Formato de la respuesta
Vender una pizza por R$ 25
cURL
curl -X POST https://novadao.app/api/v1/payment-links \
-H "Authorization: Bearer $NOVA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Pizza margherita",
"mode": "fixed",
"amount_brl": 25,
"depix_address": "lq1..."
}' Node.js
const res = await fetch('https://novadao.app/api/v1/payment-links', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOVA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Pizza margherita',
mode: 'fixed',
amount_brl: 25,
depix_address: 'lq1...',
}),
});
const link = await res.json();
console.log(link.url); Python
import os, httpx
res = httpx.post(
'https://novadao.app/api/v1/payment-links',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
json={
'name': 'Pizza margherita',
'mode': 'fixed',
'amount_brl': 25,
'depix_address': 'lq1...',
},
)
link = res.json()
print(link['url']) Devuelve
{
"id": "lk_HQKFuN22r39cc1Dp",
"name": "Pizza margherita",
"mode": "fixed",
"status": "active",
"url": "https://novadao.app/c/myshop/pizza-margherita",
"handle": "myshop",
"slug": "pizza-margherita",
"amount_brl": 25,
"min_brl": null,
"max_brl": null,
"depix_address": "lq1...",
"created_at": "2026-04-25T12:34:56.000Z"
} Consultar un enlace de pago.
Devuelve el estado actual de un enlace de pago, incluyendo la URL pública.
Parámetros
- id obligatorio
El id del enlace.
Formato de la respuesta
Consultar un enlace antes de mostrarlo
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp
Node.js
const res = await fetch('https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp', {
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const link = await res.json();
console.log(link.status, link.url); Python
import os, httpx
res = httpx.get(
'https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
link = res.json()
print(link['status'], link['url']) Devuelve
{
"id": "lk_HQKFuN22r39cc1Dp",
"name": "Pizza margherita",
"mode": "fixed",
"status": "active",
"url": "https://novadao.app/c/myshop/pizza-margherita",
"handle": "myshop",
"slug": "pizza-margherita",
"amount_brl": 25,
"min_brl": null,
"max_brl": null,
"depix_address": "lq1...",
"created_at": "2026-04-25T12:34:56.000Z"
} Pausar un enlace de pago.
Borrado suave: pausa el enlace para que deje de aceptar pagos nuevos. Las URLs ya repartidas muestran una página "pausado" en vez de 404, así los clientes en pleno checkout ven un mensaje claro.
Parámetros
- id obligatorio
El id del enlace.
Formato de la respuesta
Pausar un enlace
cURL
curl -X DELETE https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp \ -H "Authorization: Bearer $NOVA_API_KEY"
Node.js
const res = await fetch('https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp', {
method: 'DELETE',
headers: { Authorization: `Bearer ${process.env.NOVA_API_KEY}` },
});
const link = await res.json();
console.log(link.status); Python
import os, httpx
res = httpx.delete(
'https://novadao.app/api/v1/payment-links/lk_HQKFuN22r39cc1Dp',
headers={'Authorization': f'Bearer {os.environ["NOVA_API_KEY"]}'},
)
link = res.json()
print(link['status']) Devuelve
{
"id": "lk_HQKFuN22r39cc1Dp",
"name": "Pizza margherita",
"mode": "fixed",
"status": "paused",
"url": "https://novadao.app/c/myshop/pizza-margherita",
"handle": "myshop",
"slug": "pizza-margherita",
"amount_brl": 25,
"min_brl": null,
"max_brl": null,
"depix_address": "lq1...",
"created_at": "2026-04-25T12:34:56.000Z"
} Listar webhooks.
Devuelve todos los webhooks de tu cuenta.
Formato de la respuesta
Listar webhooks configurados
cURL
curl -H "Authorization: Bearer $NOVA_API_KEY" \ https://novadao.app/api/v1/webhooks
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
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'])) Devuelve
{
"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"
}
]
} Suscribirse a eventos.
Crea un suscriptor de webhook. La respuesta incluye un `signing_secret` que se muestra una sola vez — guárdalo; lo vas a necesitar para verificar la firma de cada entrega.
Parámetros
- url obligatorio
Endpoint HTTPS que recibirá las entregas vía POST.
- events obligatorio
Uno o más de: pix.charge.created, pix.charge.received, pix.charge.paid, pix.charge.expired, pix.charge.failed, payment_link.paid.
Formato de la respuesta
Suscribirse a cobros pagados
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
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
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']) Devuelve
{
"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"
} Cancelar la suscripción.
Elimina un suscriptor de webhook. Las entregas ya encoladas igual se intentan.
Parámetros
- id obligatorio
Id del webhook.
Formato de la respuesta
Eliminar un webhook
cURL
curl -X DELETE https://novadao.app/api/v1/webhooks/7 \ -H "Authorization: Bearer $NOVA_API_KEY"
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
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()) Devuelve
{
"id": 7,
"deleted": true
} Disparar una entrega de prueba.
Encola una entrega `webhook.test.ping` hacia la URL de tu webhook. Úsala para confirmar que tu endpoint recibe y verifica firmas correctamente. La entrega se firma igual que un evento real.
Parámetros
- id obligatorio
Id del webhook.
Formato de la respuesta
Enviar un evento firmado de prueba
cURL
curl -X POST https://novadao.app/api/v1/webhooks/7/test \ -H "Authorization: Bearer $NOVA_API_KEY"
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
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']) Devuelve
{
"delivery_id": "whd_abc123",
"queued_at": "2026-04-25T12:34:56.000Z"
} Webhooks: eventos #
Nova publica un payload JSON a la URL de tu webhook cuando algo le pasa a uno de tus cobros o enlaces de pago. Suscríbete a los eventos que te importan al crear el webhook.
Para cobros PIX D+2, usa pix.charge.received para confirmar que el pagador completó el PIX en el riel bancario. Un pago delayed llega con status: "delayed", settlement_status: "delayed", delay_until, confirmed_at y blockchain_tx_id: null. Usa pix.charge.paid solo para la liquidación final en Liquid con blockchain_tx_id, y sigue escuchando pix.charge.failed para cobros reembolsados, cancelados o con error.
- pix.charge.created
Se acaba de crear un cobro PIX. Útil para registrar la creación en tu propia base de datos.
- pix.charge.received
Un cliente pagó el cobro PIX en el riel bancario. Los cobros D+2 pueden llegar con `status: "delayed"`, `settlement_status: "delayed"`, `delay_until`, `confirmed_at` y `blockchain_tx_id: null`, y liquidar después vía `pix.charge.paid`.
- pix.charge.paid
El cobro PIX liquidó en Liquid e incluye el id de la transacción de liquidación.
- pix.charge.expired
Un cobro PIX expiró antes de ser pagado.
- pix.charge.failed
Un cobro PIX falló (cancelado, reembolsado o rechazado por la red). Sigue escuchando este evento incluso después de `pix.charge.received`.
- payment_link.paid
Un checkout de enlace de pago se completó. Se dispara junto con `pix.charge.paid` cuando el cobro vino de un enlace.
Cada entrega incluye estos encabezados:
Nova-Signature: t=1714000000,v1=8a7c3b... Nova-Event: pix.charge.paid Nova-Delivery-Id: whd_abc123 Nova-Api-Version: 2026-04-24
Webhooks: verificar la firma #
Te mandamos un sello; tú confirmas que el sello coincide antes de confiar en el mensaje. Sin verificación, cualquiera que conozca tu URL podría enviarte eventos falsos. El encabezado Nova-Signature tiene dos partes: t (timestamp Unix en segundos) y v1 (HMAC-SHA256 en hex). La cadena firmada es t + "." + raw_body. Calcula el HMAC con tu signing_secret (devuelto una sola vez al crear el webhook) y compara.
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
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
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);
} Webhooks: política de retry #
Si tu endpoint responde 2xx en menos de 15 segundos, la entrega se marca como completa. Cualquier otra respuesta es una falla. Las fallas reintentan seis veces con backoff:
Tras el último intento, la entrega se marca como failed. Tu panel la muestra bajo "Entregas de webhook" con un botón de replay manual. La app Nova también deja una notificación en tu bandeja.
Versionado #
La versión actual es /api/v1/. La tratamos como un contrato permanente. Los campos y tipos de evento nuevos son aditivos y no requieren que cambies nada. Si alguna vez sale un cambio rompedor, aterriza en /api/v2/ y v1 sigue viva al menos 12 meses. Durante la ventana de sunset, toda respuesta de v1 carga un encabezado Deprecation: true y una fecha Sunset.
Cada payload de webhook también incluye un campo api_version para que los suscriptores puedan ramificar el código sin clavar fechas.
Bug bounty y divulgación responsable #
Nova recibe reportes de seguridad sobre superficies del producto y de la API. Envía hallazgos en privado a hello@novadao.app con el asunto [security] <descripción corta>. No abras issues públicas ni publiques detalles hasta que Nova confirme que la divulgación está permitida.
Alcance
- Superficies de producción de Nova bajo
https://novadao.app, incluyendo/app,/api/v1/*, flujos de cuenta y autenticación, enlaces de pago, firma y entrega de webhooks, y problemas del sitio o la documentación con impacto de seguridad. - Reportes probados solo contra cuentas, claves, webhooks, enlaces de pago y datos que sean tuyos o que tengas autorización explícita para probar.
Reglas
- Incluye una descripción clara, impacto, URL o endpoint afectado, pasos para reproducir y cualquier prueba de concepto necesaria para validar el problema.
- Mantén las pruebas al mínimo necesario. No accedas, modifiques, borres ni extraigas datos de otros usuarios. Si aparecen datos sensibles, detén la prueba y reporta de inmediato.
- La investigación de buena fe que sigue estas reglas está cubierta por safe harbor: Nova no tomará acción legal por pruebas autorizadas dentro de este programa.
Fuera de alcance
- Denial-of-service, pruebas de carga, brute force automatizado, ingeniería social, phishing, ataques físicos, spam o campañas de abuso.
- Proveedores externos, sistemas de partners, extensiones de navegador, dispositivos de usuarios y entornos que no sean producción, salvo autorización explícita de Nova.
- Duplicados conocidos, salida de scanner sin impacto demostrado, problemas teóricos sin prueba de concepto práctica y problemas que solo afectan navegadores o plataformas desactualizados.
Recompensas
Los reportes validados pueden ser elegibles para recompensas discrecionales. La decisión considera severidad, impacto real, reproducibilidad, explotabilidad, calidad del reporte y si el problema ya era conocido.
Soporte
Dos formas de contactarnos:
- Bot de soporte en Telegram — la vía más rápida, respondemos en menos de una hora en horario hábil.
- Incluye el
request_iddel envelope de error cuando escribas. Con eso podemos rastrear cada llamada al instante.
Para integraciones asistidas por LLM, apunta tu herramienta a /es/devs/llms.md — mismo contenido, formato legible por máquina.