Inicio rápido
De cero a un webhook verificado en cinco minutos, todo en el entorno de prueba.
En esta guía creas una llave de prueba, confirmas a qué cuenta pertenece, das de alta un cliente, le armas una cotización, se la envías y recibes los avisos de Cord en tu localhost con la firma verificada. Nada de lo que hagas aquí toca tus datos reales: una llave sk_test_ trabaja sobre el entorno de prueba de tu cuenta.
El correo de prueba sí sale
Enviar una cotización de prueba manda un correo real al cliente, con el asunto marcado como prueba. Usa un correo tuyo en el paso 4.
-
Crea una llave de prueba. En la app, activa el Modo desarrollador en Tu cuenta › Preferencias y abre la píldora Desarrolladores en la esquina inferior derecha. En la pestaña Llaves, crea una Secret Key de lectura y escritura en modo prueba (si la app ya está en modo prueba, la llave nace de prueba). Empieza con
sk_test_y se muestra una sola vez: cópiala.La app en el entorno de prueba con el Workbench abierto en la pestaña Llaves: la llave secreta de lectura y escritura recién creada aparece completa una sola vez, sk_test_ seguida de 48 caracteres, con su botón Copiar; detrás, la fila nueva ya figura en la tabla como Prueba y Secreta. Guárdala en una variable de tu terminal. El CLI de Cord también la lee de ahí:
export CORD_API_KEY="sk_test_..." -
Confirma la llave.
GET /api/v1/meresponde con la organización, el permiso y el modo de la llave:curl https://cordhq.app/api/v1/me \ -H "Authorization: Bearer $CORD_API_KEY"{ "data": { "org": { "id": "5b1f7d9e-3c2a-4e8b-9f61-0a7c4d2e8b13", "nombre": "Aceros del Norte", "plan": "pro" }, "scope": "write", "mode": "test" } }Si ves
"mode": "test", todo lo que sigue ocurre en tu entorno de prueba. Un401concode: "invalid_key"significa que la llave está mal copiada o fue revocada. -
Escucha los webhooks en tu máquina. Cord no puede llamar a tu
localhost, así que el CLI abre una sesión de escucha y te reenvía cada evento con los headers de firma reales. Primero, el servidor que los recibe. Instala el SDK en una carpeta vacía:npm init -y && npm install @flouviahq/nodeGuarda esto como
server.mjs:import { createServer } from 'node:http'; import { constructEvent, CordWebhookSignatureError } from '@flouviahq/node'; createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/webhooks/cord') { res.writeHead(404).end(); return; } // La firma se calcula sobre el cuerpo CRUDO: no lo parses antes de verificar. const chunks = []; for await (const chunk of req) chunks.push(chunk); try { const event = await constructEvent(Buffer.concat(chunks), req.headers, process.env.CORD_WEBHOOK_SECRET); console.log(event.event, event.id, event.data.id); res.writeHead(200).end('ok'); } catch (err) { if (err instanceof CordWebhookSignatureError) { console.error('Firma rechazada:', err.code); res.writeHead(400).end(err.code); return; } throw err; } }).listen(3000, () => console.log('Escuchando en http://localhost:3000/webhooks/cord'));En una segunda terminal (con la misma
CORD_API_KEY), abre la sesión de escucha:npx @flouviahq/cli listen --forward-to http://localhost:3000/webhooks/cordEl CLI imprime el secreto de la sesión (
whsec_…). Arranca tu servidor con él en la primera terminal:CORD_WEBHOOK_SECRET="whsec_..." node server.mjsDeja ambas corriendo. La sesión vence en 24 horas y Ctrl+C la cierra.
-
Crea un cliente. Con un correo tuyo:
curl https://cordhq.app/api/v1/clientes \ -H "Authorization: Bearer $CORD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "empresa": "Ferretería La Prueba", "contacto": "Ana Torres", "email": "tu-correo@ejemplo.com" }'{ "data": { "id": "0e6f2b8a-91c4-4d7e-b3a5-6c2d8f1e4a90" } }Tu servidor imprime el primer evento:
client.created evt_4c1e9a7b2d5f8e0a3b6c9d2e5f8a1b4c 0e6f2b8a-91c4-4d7e-b3a5-6c2d8f1e4a90 -
Crea la cotización. Usa el
iddel cliente. Sintax_rate, cada línea toma la tasa predeterminada del catálogo de impuestos de tu cuenta; sinbase_currency, la cotización va en la divisa de tu cuenta.curl https://cordhq.app/api/v1/cotizaciones \ -H "Authorization: Bearer $CORD_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6a1d3f5e-8b2c-4e7a-9d0f-1c3e5a7b9d2f" \ -d '{ "cliente_id": "0e6f2b8a-91c4-4d7e-b3a5-6c2d8f1e4a90", "items": [ { "descripcion": "Instalación de cancelería", "cantidad": 1, "precio_unitario": 12000 } ], "vigencia_dias": 15 }'{ "data": { "id": "8b0c6f0e-2d55-4c1e-9a43-6a2f1c7d9e10", "token": "3f9a1c2e7b4d6f8a0b1c2d3e4f5a6b7c", "folio": "COT-0001", "link_publico": "https://cordhq.app/q/3f9a1c2e7b4d6f8a0b1c2d3e4f5a6b7c", "needs_approval": false, "motivo": null } }La cotización nace como borrador y tu servidor recibe
quote.created. El headerIdempotency-Keyhace seguro reintentar: con la misma clave y el mismo cuerpo, Cord devuelve esta misma respuesta en vez de crear una segunda cotización. Genera una clave nueva (por ejemplo conuuidgen) para cada cotización distinta. -
Envíala. El envío genera el link y manda el correo al cliente:
curl https://cordhq.app/api/v1/cotizaciones/8b0c6f0e-2d55-4c1e-9a43-6a2f1c7d9e10 \ -H "Authorization: Bearer $CORD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "send" }'{ "data": { "ok": true, "status": "sent", "email": { "sent": true } } }Si el cliente no tuviera correo,
emailllegaría como{ "sent": false, "skipped": "el cliente no tiene correo" }y la cotización quedaría enviada de todos modos. Tu servidor recibe:quote.sent evt_9d2a6c4e1f8b3a5d7c0e2f4a6b8d1c3e 8b0c6f0e-2d55-4c1e-9a43-6a2f1c7d9e10 -
Simula al cliente. Abre el link desde el correo o provoca la apertura con un simulador. En otra terminal:
npx @flouviahq/cli simulate quote 8b0c6f0e-2d55-4c1e-9a43-6a2f1c7d9e10 vistaLlega
quote.viewed. Para cerrar el ciclo, apruébala desde la API ({ "action": "approve" }) y llegaráquote.approved.
Qué contiene cada aviso
Cada entrega es un POST con este cuerpo. id es la identidad del evento (igual en cada reintento: deduplica con él) y data es el resumen de la cotización:
{
"id": "evt_9d2a6c4e1f8b3a5d7c0e2f4a6b8d1c3e",
"event": "quote.sent",
"created_at": "2026-10-07T17:04:12.318Z",
"data": {
"id": "8b0c6f0e-2d55-4c1e-9a43-6a2f1c7d9e10",
"folio": "COT-0001",
"status": "sent",
"moneda": "MXN",
"total": 13920,
"cliente": "Ferretería La Prueba",
"cliente_id": "0e6f2b8a-91c4-4d7e-b3a5-6c2d8f1e4a90",
"link_publico": "https://cordhq.app/q/3f9a1c2e7b4d6f8a0b1c2d3e4f5a6b7c"
}
}
El total de este ejemplo incluye el IVA del 16 % de una cuenta mexicana; en tu cuenta saldrá con tus tasas. Todo importe viaja con su moneda.
Lo mismo con el SDK de Node
import { Cord } from '@flouviahq/node';
const cord = new Cord(process.env.CORD_API_KEY);
const me = await cord.me();
const cliente = await cord.clients.create({ empresa: 'Ferretería La Prueba', email: 'tu-correo@ejemplo.com' });
const cotizacion = await cord.quotes.create({
cliente_id: cliente.id,
items: [{ descripcion: 'Instalación de cancelería', cantidad: 1, precio_unitario: 12000 }],
});
await cord.quotes.send(cotizacion.id);
console.log(me.mode, cotizacion.folio, cotizacion.link_publico);
El SDK manda una Idempotency-Key en cada operación que escribe y la repite en sus reintentos. Ver SDK de Node.
Siguientes pasos
- Webhooks: registra un endpoint real, reintentos, rotación de secreto y verificación manual de la firma.
- Eventos: los 41 tipos de evento y el historial por API.
- API de Cotizaciones: aprobar, rechazar, marcar pagada y borrar.
- Manejo de errores: todos los códigos y cuándo reintentar.
- Antes de producción, crea una llave
sk_live_y registra tu endpoint en el Workbench: el secreto decord listensolo sirve para la sesión.