Saltar al contenido

Cotizaciones

El corazón transaccional de Cord. Crea propuestas, obtén sus links públicos y adminístralas.

Descripción General

El objeto Cotizacion (quote) representa una propuesta comercial hacia un cliente. Usando la API de Cord, puedes inyectar cotizaciones directamente desde un ERP, CRM o generar flujos automatizados de carritos de compra.

Todas las cotizaciones generadas vía API quedan registradas en el registro de auditoría (audit_log) etiquetadas automáticamente como provenientes de tu Clave de API.

Crear una Cotización

Para crear un borrador de cotización, debes realizar un POST /api/v1/cotizaciones enviando el detalle comercial.

Petición:

curl -X POST "https://cordhq.app/api/v1/cotizaciones" \
     -H "Authorization: Bearer sk_live_tU..." \
     -H "Content-Type: application/json" \
     -d '{
       "cliente_id": "8f7b2c1e-4d3a-4b9f-a6e2-1c5d7e9f0a3b",
       "terminos": "net30",
       "vigencia_dias": 15,
       "notas": "Proyecto anual",
       "base_currency": "EUR",
       "items": [
         {
           "descripcion": "Licencia anual — plan Pro",
           "cantidad": 12,
           "precio_unitario": 500
         }
       ]
     }'

Cada línea de items requiere descripcion, cantidad y precio_unitario. producto_id es opcional (referencia un producto de tu catálogo para heredar su nombre y precio de lista) y precio_negociado permite capturar un precio con descuento distinto al de lista. No existe un campo sku: si quieres enlazar la línea a un producto, usa su producto_id.

Campos de la cotización

Campo Detalle
items Obligatorio. Las líneas (ver la tabla siguiente).
cliente_id o cliente Un cliente de tu directorio, o los datos para darlo de alta (empresa, y opcionales email, contacto, telefono, rfc y fiscal).
terminos contado, net7, net15, net30, net45, net60 o net90 (N días naturales). Sin él, los términos del cliente o los de tu cuenta.
vigencia_dias Días que la cotización es válida desde su envío.
notas Notas para el cliente.
base_currency Divisa de venta (ISO 4217).
fiscal_currency Divisa contable; si difiere de la de venta, la factura declara el tipo de cambio.
iva_incluido true si los precios ya incluyen el impuesto.
descuento Descuento de documento antes de impuestos: { "tipo": "porcentaje", "valor": 10 } o { "tipo": "monto", "valor": 2500 }. Cord calcula el importe y lo reparte entre las líneas; un monto se topa en el subtotal. null lo quita. Ver Descuentos y cupones.
cupon Código de un cupón de tu cuenta (por ejemplo, "BIENVENIDA10"). Cord valida vigencia, divisa y usos; si viene, manda sobre descuento.
plan_pagos Plan de pagos por hitos: de 2 a 24 pagos, cada uno con su importe (porcentaje, monto o resto) y su vence (al_aprobar, dias, fecha o al_cumplir), y, con cliente_empresa: true, los intereses. Mandas la definición, nunca importes: Cord los calcula y agrega la línea de intereses. No se combina con anticipo ni iguala. Ver Plan de pagos por hitos.
send true para enviarla al crearla. Solo con llave secreta.
Campo de la línea Detalle
descripcion, cantidad, precio_unitario Obligatorios.
producto_id Producto del catálogo.
precio_negociado Precio final de la línea, si es distinto al de lista.
costo_unitario Costo interno para calcular el margen. Nunca lo ve el cliente.
tax_rate Fracción de 0 a 1 (0.16, no 16), validada contra tu catálogo de impuestos. 0 significa exenta a propósito.
exemption_reason Solo con tax_rate 0. España: la causa de exención o no sujeción de Verifactu (E1–E6, N1, N2, S2). Resto de la UE: el código VATEX de la factura electrónica.
clave_sat, clave_unidad_sat Solo México: clave de producto o servicio del SAT (8 dígitos) y clave de unidad de esa línea. Ganan sobre las del producto; una clave con forma inválida responde 400. Fuera de México se ignoran.
curl -X POST "https://cordhq.app/api/v1/cotizaciones" \
     -H "Authorization: Bearer sk_live_tU..." \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: cotizacion-pedido-4471" \
     -d '{
       "cliente_id": "8f7b2c1e-4d3a-4b9f-a6e2-1c5d7e9f0a3b",
       "terminos": "net45",
       "base_currency": "MXN",
       "cupon": "BIENVENIDA10",
       "items": [
         { "descripcion": "Cemento gris CPC 30R", "cantidad": 400, "precio_unitario": 245,
           "tax_rate": 0.16, "clave_sat": "30111601", "clave_unidad_sat": "H87" }
       ]
     }'

Con una llave publicable (la de tu sitio) Cord ignora descuento, precio_negociado, costo_unitario y plan_pagos: el único descuento que acepta es un cupon. Un cupón que no existe responde 404 con code: "coupon_no_existe"; uno inactivo, vencido, agotado, de otra divisa o limitado por cliente sin cliente responde 409 con coupon_inactivo, coupon_vencido, coupon_aun_no_vigente, coupon_agotado, coupon_agotado_cliente, coupon_moneda o coupon_requiere_cliente.

base_currency es la divisa en la que el cliente ve y paga la cotización (ISO 4217, ej. MXN, USD, EUR, GBP). Es opcional — si la omites, Cord usa la divisa contable de tu organización — pero la API es agnóstica de país: una cuenta en Madrid, Austin o Bogotá cotiza en su propia divisa sin ningún parámetro adicional.

Respuesta Exitosa:

Cord te devuelve el id (UUID) de la cotización, su token público y el link público (link_publico) ya construido como URL absoluta, para que se lo envíes a tu cliente inmediatamente vía WhatsApp, SMS o un motor de correos externo. Si tu organización tiene un dominio activo en Cord Domains, el link usa ese dominio en lugar de cordhq.app.

{
  "data": {
    "id": "2a9d8e4f-6b1c-4d7a-9e3f-5c8b1a2d4e6f",
    "folio": "COT-00104",
    "token": "9f3b6c2a7e1d4f8b0a5c3e7d1b9f2a46",
    "link_publico": "https://cordhq.app/q/9f3b6c2a7e1d4f8b0a5c3e7d1b9f2a46",
    "needs_approval": false
  }
}

Si la cotización requiere aprobación interna antes de enviarse (por ejemplo, un descuento fuera de umbral), needs_approval viene en true y motivo describe la razón.

Listar Cotizaciones

Puedes recuperar tu historial paginado de cotizaciones o filtrar por estado (por ejemplo, buscar todas las cotizaciones enviadas o pagadas).

Petición:

curl -X GET "https://cordhq.app/api/v1/cotizaciones?status=paid&limit=10" \
     -H "Authorization: Bearer sk_live_tU..."

El valor de status es el estado interno tal cual (en inglés, no traducido): draft, sent, viewed, approved, rejected, expired, paid o invoiced. Para buscar una cotización por su folio usa folio=COT-00104: compara el folio completo sin distinguir mayúsculas. Para ver solo las de un cliente usa cliente_id=<uuid>. Cada cotización trae su cliente_id: null si no tiene cliente.

{
  "data": [
    {
      "id": "2a9d8e4f-6b1c-4d7a-9e3f-5c8b1a2d4e6f",
      "folio": "COT-00104",
      "cliente": "Stark Industries",
      "cliente_id": "8f7b2c1e-4d3a-4b9f-a6e2-1c5d7e9f0a3b",
      "status": "paid",
      "total": 6000,
      "moneda": "MXN",
      "terminos": "Net 30",
      "terminos_codigo": "net30",
      "vigencia": "10 sep 2026",
      "creada": "20 ago 2026",
      "vigencia_iso": "2026-09-10",
      "creada_iso": "2026-08-20T17:42:10.000Z",
      "link_publico": "https://cordhq.app/q/9f3b6c2a7e1d4f8b0a5c3e7d1b9f2a46",
      "language": "es"
    }
  ],
  "meta": { "limit": 10, "offset": 0, "total": 1, "next_cursor": null }
}

Fechas y términos en formato de presentación

En la lista y el detalle, terminos llega como etiqueta en el idioma de tu cuenta (Contado o Due on receipt, Net 45) y vigencia y creada como fechas de presentación (10 sep 2026). No las parses: para ordenar o comparar usa vigencia_iso (AAAA-MM-DD) y creada_iso (ISO 8601). Para comparar el plazo usa terminos_codigo (contado, net7, net15, net30, net45, net60 o net90), que no cambia con el idioma; para la fecha exacta de cada cambio usa created_at del historial de eventos con object_id.

En el listado, link_publico también es una URL absoluta, igual que en la respuesta de creación. Para el detalle de una sola cotización (líneas, eventos, aprobación), usa GET /api/v1/cotizaciones/{id}.

language es el idioma en que el cliente recibe la cotización: es, en, pt, fr o de. Es de solo lectura: la cotización lo toma al enviarse, del cliente o, si no tiene uno, de tu organización, y una versión nueva lo conserva. Vale null mientras es borrador. Para enviarla en otro idioma, cambia el language del cliente antes de enviarla.

El detalle trae además descuento: null si no hay, o el descuento de documento con su definición, el cupón del que salió y el importe calculado antes de impuestos:

"descuento": { "tipo": "porcentaje", "valor": 10, "cupon": "BIENVENIDA10", "monto": 9800, "moneda": "MXN" }

total ya incluye el descuento.

Y trae plan_pagos: null sin plan, o el plan con el precio de contado, los intereses, el total y la tasa anual efectiva (contado, intereses, total, tasa_anual_efectiva, en moneda) y cada pago con su importe, su vence, si está habilitado y si es exigible hoy. Mientras tu cliente no aprueba, trae el plan como se guardó; el link lo muestra calculado con el día de hoy y, al aprobar, queda el del día de la aprobación.

Proponer líneas con IA

POST /api/v1/cotizaciones/ia recibe { "texto": "..." } (el pedido del cliente, hasta 6000 caracteres) y devuelve las líneas emparejadas con tu catálogo, sin crear nada:

{ "data": { "moneda": "MXN", "items": [{ "id": "7e3c9a1f-2b4d-4c6e-8f0a-b1d3e5f7a9c2", "nombre": "Cemento Gris 50kg", "unidad": "saco", "lista": 185, "negociado": null, "cantidad": 5 }] } }

id es null cuando el concepto no está en tu catálogo. Los precios van en moneda. Para crear la cotización, manda esas líneas a POST /api/v1/cotizaciones. Requiere una llave con escritura y gasta una unidad de IA de tu plan: si ya no quedan, recibes 429 con code: "ai_quota_exceeded"; si el texto no trae productos, 422 con code: "no_items".

Cambiar el estado de una cotización

Con una llave de escritura (write) puedes mover la cotización por su ciclo de vida con POST /api/v1/cotizaciones/{id}:

curl -X POST "https://cordhq.app/api/v1/cotizaciones/2a9d8e4f-6b1c-4d7a-9e3f-5c8b1a2d4e6f" \
     -H "Authorization: Bearer sk_live_tU..." \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: aprobar-COT-00104" \
     -d '{ "action": "approve" }'
action Desde el estado Resultado
send draft Envía la cotización al cliente (correo y link) en el idioma de sus documentos, que la cotización guarda en language. En el plan Gratis consume uno de los envíos del mes.
resend sent, viewed, expired La reenvía al cliente.
approve sent, viewed La marca como aprobada. Si su plan de pagos lleva intereses, responde 409 aprobacion_solo_cliente: esa la aprueba tu cliente desde su link.
reject sent, viewed La marca como rechazada.
mark_paid approved, invoiced Registra el pago. Acepta payment_method opcional (por defecto transferencia). Los cobros en línea pendientes de esa cotización se cancelan.

La respuesta es { "data": { "ok": true, "status": "approved" } }. Si la cotización no está en un estado desde el que se pueda aplicar la acción, o si otra petición la cambió al mismo tiempo, recibes 409 con code: "invalid_state" y no se aplica nada. Cada cambio queda en el registro de auditoría con tu llave como actor y dispara su evento de webhook.

Emitir la factura no está disponible por esta ruta: las facturas se crean y emiten con POST /api/v1/facturas y POST /api/v1/facturas/{id}.

Editar un borrador

PATCH /api/v1/cotizaciones/{id} cambia una cotización que todavía es borrador (draft): lo que mandas reemplaza a lo guardado y lo que no mandas se conserva. items reemplaza todas las líneas; sin items se conservan las que tiene, con su impuesto. cliente_id: null deja el borrador sin cliente y notas: null borra las notas. Acepta los mismos campos que la creación salvo cliente y send; para enviarlo usa { "action": "send" }.

curl -X PATCH "https://cordhq.app/api/v1/cotizaciones/2a9d8e4f-6b1c-4d7a-9e3f-5c8b1a2d4e6f" \
     -H "Authorization: Bearer sk_live_tU..." \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: editar-COT-00104-1" \
     -d '{ "terminos": "net30", "descuento": { "tipo": "porcentaje", "valor": 5 } }'

Cord recalcula los totales con las mismas reglas que al crearla (impuesto por línea, descuento, cupón y plan de pagos) y responde el detalle actualizado, igual que GET /api/v1/cotizaciones/{id}. Un campo con el tipo equivocado responde 400 con code: "invalid_request" y field, y una cotización que ya se envió responde 409 con code: "invalid_state"; en los dos casos no cambia nada.

Eliminar un borrador

DELETE /api/v1/cotizaciones/{id} elimina una cotización que nunca se envió (draft). Cualquier otro estado responde 409.