Antes de empezar

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.

Autenticación

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"

Límites

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.

Consultar datos

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/pagos

La respuesta trae los datos y por dónde seguir:

{
  "datos": [ { "id": "8f3c…", "nombre": "Harina Pan 1 kg", "precio": 1.2, … } ],
  "siguiente": "8f3c…"
}

Paginación

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 anterior

Es 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.

Registrar una venta

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 }

La clave única es obligatoria

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.

Los renglones

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.

El cliente

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.

Una factura necesita a quién se le emite

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 ficha

Si 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.

Crear o completar un cliente, sin vender

POST /api/v1/clientes
{ "nombre": "Ferretería El Tornillo", "rif": "J-30123456-7", "ciudad": "Valencia" }

→ { "cliente_id": "d47a…" }

Errores

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" }
HTTPerrorQué pasó
401api_no_autorizadaLa llave no existe, fue revocada, o no tiene el permiso que la operación pide.
403sin_permisoQuien creó la llave ya no tiene permiso para esa operación dentro de Nébula.
400api_invalidaFalta un dato o viene mal: el mensaje dice cuál.
409sin_existenciaNo hay suficiente inventario para lo que se quiere vender.
429api_limiteMá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.
500error_internoAlgo falló de nuestro lado. No trae detalles a propósito; queda registrado en nuestro servidor.

Webhooks

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 cero

Cada 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 webhook

Tu 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.

Verificar la firma

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);

Qué ha cambiado

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.

¿Dudas?

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.

Tu contabilidad en piloto automático.Tus declaraciones, en manos expertas.