Todo pasa por https://nebulave.app/api/v1. Las respuestas son JSON y las peticiones que escriben mandan JSON. Las llaves se crean dentro de Nébula, en Ajustes → API e integraciones, y se muestran una sola vez: si se pierde una, se revoca y se crea otra.
Una llave puede ser solo de lectura o de lectura y escritura. Dale una llave distinta a cada integración: así revocar la de la tienda no deja sin servicio a lo demás.
La llave va en la cabecera Authorization, y nunca en la dirección: lo que va en la URL queda escrito en los registros de cualquier servidor por el que pase.
curl https://nebulave.app/api/v1/productos \
-H "Authorization: Bearer neb_tu_llave_aqui"120 peticiones por minuto por llave. Al pasarse, la respuesta es 429 con api_limite: hay que esperar al minuto siguiente. Si tu integración necesita más, escríbenos antes de repartir el trabajo entre varias llaves.
Cuatro recursos, todos con la misma forma:
GET /api/v1/productos?limite=50
GET /api/v1/clientes
GET /api/v1/documentos → facturas y notas (los presupuestos no entran), con sus renglones
GET /api/v1/pagosLa respuesta trae los datos y por dónde seguir:
{
"datos": [ { "id": "8f3c…", "nombre": "Harina Pan 1 kg", "precio": 1.2, … } ],
"siguiente": "8f3c…"
}limite va entre 1 y 100 (50 por defecto). Cuando hay más, la respuesta trae siguiente: se lo pasas como cursor para la página que sigue. Cuando siguiente viene en null, ya no queda nada.
GET /api/v1/productos?limite=100
GET /api/v1/productos?limite=100&cursor=8f3c… ← el "siguiente" de la respuesta anteriorEs paginación por cursor y no por número de página a propósito: si alguien carga un producto mientras recorres el catálogo, no se te repite ni se te salta ninguno.
POST /api/v1/ventas, con una llave de escritura.
curl -X POST https://nebulave.app/api/v1/ventas \
-H "Authorization: Bearer neb_tu_llave_aqui" \
-H "Content-Type: application/json" \
-d '{
"clave": "woo-10457",
"tipo": "nota",
"cliente": { "nombre": "María Pérez", "rif": "V-12345678", "email": "[email protected]" },
"items": [
{ "sku": "ACE-001", "cantidad": 2 },
{ "sku": "HAR-001", "cantidad": 3, "precio": 1.15 }
],
"pagos": [ { "metodo": "pago_movil", "monto": 5.75, "referencia": "004512" } ],
"notas": "Pedido #10457 de la tienda"
}'Respuesta:
{ "documento_id": "b21e…", "numero": "NE-00042", "total": 5.75, "saldo": 0 }clave (4 caracteres o más) es el número del pedido en tu sistema. Sirve para que un reenvío no registre la venta dos veces: si mandas la misma clave otra vez, Nébula devuelve el documento que ya existía con "repetida": true y no crea nada.
Por eso, ante un error de red se reintenta con la MISMA clave, nunca con una nueva. Es la diferencia entre un reintento y una venta duplicada.
Cada renglón identifica su producto por sku o por producto_id, y lleva cantidad. Lo demás es opcional y sale del producto si no lo mandas: precio, nombre y iva_tasa. Si el producto no es de tu empresa o la cantidad no es mayor que cero, la respuesta es 400.
Puedes mandar cliente_id si ya lo tienes, o el objeto cliente y dejar que Nébula lo resuelva. Se identifica por RIF o por correo, en ese orden, y hace falta al menos uno de los dos — es lo que evita que cada pedido cree un cliente repetido.
"cliente": {
"nombre": "María Pérez", ← obligatorio solo si hay que crearlo
"rif": "V-12345678", ← con RIF o con correo alcanza
"email": "[email protected]",
"telefono": "0414-0000000",
"direccion": "Av. Principal, local 4",
"ciudad": "Caracas"
}Si el cliente ya existe, no se pisa lo que la empresa tenga cargado: solo se completan los campos vacíos. El nombre que ya estaba se respeta.
Es la ley venezolana (artículo 8 de la Providencia 0071): toda factura identifica a quien la recibe. Una nota de entrega y un presupuesto no lo exigen.
Con "tipo": "factura" tienes tres formas de cumplirlo, y basta con una:
"cliente": { "nombre": "María Pérez", "rif": "V-12345678" } ← el RIF en el cliente
"cliente_id": "d47a…" ← un cliente que YA tiene RIF en su ficha
"receptor_documento": "V-12345678" ← consumidor final, sin fichaSi no mandas ninguna, la respuesta es 400 con el motivo exacto:
{ "error": "venta_invalida: una factura necesita el RIF o la cédula de quien la recibe" }
{ "error": "venta_invalida: el cliente X no tiene RIF ni cédula en su ficha…" }El documento lleva su letra —V, E, J, G o P— y Nébula lo normaliza: V12345678 se guarda como V-12345678. Los dígitos solos se rechazan, porque no dicen si es una cédula venezolana, una extranjera o un RIF.
POST /api/v1/clientes
{ "nombre": "Ferretería El Tornillo", "rif": "J-30123456-7", "ciudad": "Valencia" }
→ { "cliente_id": "d47a…" }Siempre JSON, con el motivo y un mensaje para quien integra:
{ "error": "api_invalida", "mensaje": "identifica al cliente con su RIF o su correo, para no duplicarlo" }Regla general: un 422 es una regla del negocio que rechazó la operación, y el mensaje te dice cómo resolverla — no lo trates como un fallo nuestro ni lo reintentes igual. Un 500 sí es nuestro: ese sí conviene reintentarlo (con la misma clave) y, si insiste, escribirnos.
{ "error": "credito_no_aprobado",
"mensaje": "Tienda de Prueba no tiene crédito aprobado; registra el pago completo o asígnale un límite de crédito en su ficha" }| HTTP | error | Qué pasó |
|---|---|---|
| 401 | api_no_autorizada | La llave no existe, fue revocada, o no tiene el permiso que la operación pide. |
| 403 | sin_permiso | Quien creó la llave ya no tiene permiso para esa operación dentro de Nébula. |
| 400 | api_invalida | Falta un dato o viene mal: el mensaje dice cuál. |
| 409 | sin_existencia | No hay suficiente inventario para lo que se quiere vender. |
| 429 | api_limite | Más de 120 peticiones en el mismo minuto. |
| 422 | (varios) | Una regla del negocio rechazó la operación, y el mensaje dice cuál y cómo resolverla. Por ejemplo credito_no_aprobado, credito_excedido, cliente_moroso, caja_cerrada, sin_tasa, venta_invalida, referencia_requerida o pago_ya_procesado. |
| 500 | error_interno | Algo falló de nuestro lado. No trae detalles a propósito; queda registrado en nuestro servidor. |
Nébula le avisa a tu sistema cuando pasa algo, con un POST de JSON a la dirección que configures en Ajustes → API e integraciones. Tres eventos:
documento.emitido ← se emitió una factura o una nota
pago.registrado ← entró un pago
producto.sin_existencia ← un producto llegó a ceroCada aviso trae estas cabeceras:
X-Nebula-Evento: documento.emitido
X-Nebula-Entrega: 3f9a… ← id de ESTE envío; sirve para no procesar dos veces
X-Nebula-Firma: sha256=<hmac> ← firma del cuerpo con el secreto del webhookTu servidor tiene 10 segundos para responder, y cualquier 2xx cuenta como recibido. Si responde otra cosa o no responde, Nébula reintenta; el estado y el último error se ven en la misma pantalla de Ajustes.
Responde rápido y procesa después: si tu integración tarda en hacer su trabajo dentro del mismo pedido, el aviso se va a dar por fallido aunque lo hayas recibido bien.
Firma el cuerpo tal como llegó, sin volver a serializarlo: si lo conviertes a objeto y de vuelta a texto, el orden de las claves puede cambiar y la firma deja de coincidir.
JavaScript (Node / Express):
import crypto from "node:crypto";
// Ojo: el cuerpo CRUDO, no el ya interpretado.
app.post("/nebula/aviso", express.raw({ type: "application/json" }), (req, res) => {
const firmaRecibida = req.get("X-Nebula-Firma") || "";
const esperada = "sha256=" + crypto
.createHmac("sha256", process.env.NEBULA_WEBHOOK_SECRETO)
.update(req.body) // Buffer con los bytes exactos
.digest("hex");
const a = Buffer.from(firmaRecibida);
const b = Buffer.from(esperada);
// timingSafeEqual y no ===: comparar con === filtra información por el tiempo que tarda.
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("firma inválida");
}
const evento = req.get("X-Nebula-Evento");
const datos = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // primero responder…
procesarEnSegundoPlano(evento, datos); // …y después trabajar
});PHP (WooCommerce u otro):
<?php
$cuerpo = file_get_contents('php://input'); // crudo, sin tocar
$recibida = $_SERVER['HTTP_X_NEBULA_FIRMA'] ?? '';
$esperada = 'sha256=' . hash_hmac('sha256', $cuerpo, NEBULA_WEBHOOK_SECRETO);
if (!hash_equals($esperada, $recibida)) { // hash_equals, no ==
http_response_code(401);
exit('firma inválida');
}
$evento = $_SERVER['HTTP_X_NEBULA_EVENTO'] ?? '';
$datos = json_decode($cuerpo, true);
$entrega = $_SERVER['HTTP_X_NEBULA_ENTREGA'] ?? '';
// Guarda el id de entrega y descarta los repetidos: un reintento de Nebula
// puede traer un aviso que ya procesaste.
if (ya_procesado($entrega)) { http_response_code(200); exit; }
http_response_code(200);
procesar($evento, $datos);27 de octubre de 2026 · las facturas identifican a quien las reciben. Una venta con "tipo": "factura" ahora exige el RIF o la cédula del receptor: en el cliente, en la ficha del cliente_id, o en el campo nuevo receptor_documento. Sin eso, la respuesta es 400 con venta_invalida. Las notas de entrega y los presupuestos no cambian.
Escríbenos a [email protected] con el nombre de tu empresa y qué estás integrando. Si algo de esta página no coincide con lo que ves, gana lo que responda la API: cuéntanoslo y lo corregimos.