Ir al contenido

Facturas

POST/v1/invoices

Emite una factura electrónica de venta, tipo DIAN 01. Consume un consecutivo del rango de numeración que le indiques y devuelve el documento en estado QUEUED: numerar, armar el UBL, firmar y transmitir pasa después, en segundo plano.

https://sandbox.facturacion.misofk.com/v1/invoices
Authorization: Bearer <access_token>
Content-Type: application/json

Solo se acepta el esquema Bearer. Sin token, o con otro esquema, la respuesta es 401 UNAUTHORIZED.

Campo Tipo Obligatorio Descripción
reference_code string Sí Tu identificador del documento, de 1 a 100 caracteres. Es la clave de idempotencia: repetirlo devuelve la factura que ya existe.
numbering_range_id string (UUID) Sí Rango de numeración autorizado. Tiene que ser de tipo INVOICE; con uno de otro tipo la respuesta es 422. Ver rangos de numeración.
customer objeto Sí El adquirente. Campos abajo.
items arreglo Sí Al menos una línea. Campos abajo.
payment objeto No Forma y medio de pago. Si se omite, el servidor guarda contado en efectivo.
notes string No Observación libre. Va al UBL y a la representación gráfica. Es una sola cadena, no un arreglo.
send_email boolean No Si nosotros mandamos el correo al adquirente. Por defecto true. Con false la factura se emite y se valida igual, pero no sale correo: la entrega te la quedas tú.
Campo Tipo Obligatorio Descripción
identification_type string Sí Tipo de documento, tabla DIAN 6.2.1: 11 registro civil, 12 tarjeta de identidad, 13 cédula de ciudadanía, 21 tarjeta de extranjería, 22 cédula de extranjería, 31 NIT, 41 pasaporte, 42 documento extranjero, 47 PEP, 48 PPT, 50 NIT de otro país, 91 NUIP.
identification string Sí El número, sin dígito de verificación. Entra en el cálculo del CUFE. Para consumidor final: 222222222222.
dv string No Dígito de verificación. Exactamente un carácter: "8", no "08". Dos caracteres devuelven 400.
person_type string Sí 1 jurídica, 2 natural. Solo esos dos valores.
name string Sí Razón social o nombre completo.
tax_regime string No 48 responsable de IVA, 49 no responsable. Solo esos dos valores.
responsibilities string[] No Responsabilidades fiscales, tabla 6.2.7: O-13, O-15, O-23, O-47, R-99-PN.
email string No Destino del correo de entrega.
phone string No
address string No
municipality_code string No Código DANE del municipio, exactamente 5 caracteres: 05001 para Medellín. Cuatro o seis devuelven 400.

Tres detalles que cuestan una tarde si no se saben.

identification_type y responsibilities no se validan contra el catálogo. El API acepta cualquier cadena no vacía. Un tipo inventado pasa la validación aquí y lo rechaza la DIAN después, cuando la factura ya gastó un consecutivo. Los valores de la tabla son la lista buena; úsala tú.

email no se valida como correo. El campo se comprueba como cadena, no con una regla de formato. facturacion@ o facturacion.ejemplo.com entran sin protestar; el problema aparece más tarde, en email_error, con la factura ya validada y el correo sin salir. Valídalo de tu lado.

responsibilities es un arreglo, y ahora se comprueba que lo sea. Manda ["O-15"]; un "O-15" suelto devuelve 400. Antes pasaba, y salía caro: el documento se creaba, gastaba un consecutivo de tu resolución y acababa en FAILED con el número ya perdido.

Campo Tipo Obligatorio Descripción
code string Sí Código del producto o servicio en tu sistema.
name string Sí Descripción que sale en el documento.
quantity número Sí Mayor que cero. Admite fraccionarios —peso, horas—: el mínimo es 0.000001.
unit_measure string No Unidad de medida UN/ECE rec 20. Si se omite, el servidor pone 94 (Unidad). Catálogo: 94 unidad, EA unidad suelta, KGM kilogramo, GRM gramo, LTR litro, MLT mililitro, MTR metro, CMT centímetro, MTK metro cuadrado, HUR hora, DAY día, BX caja, PK paquete.
price número Sí Precio unitario y antes de impuestos. No es el total de la línea ni lleva IVA incluido. Se admite 0.
discount_rate número No Porcentaje, no monto. 10 es un 10 %. Por defecto 0.
taxes arreglo No Impuestos de la línea. Sin taxes, la línea va sin impuesto y su total es igual a su base.
Campo Tipo Obligatorio Descripción
code string Sí Tributo, tabla DIAN 6.2.2. Los de uso corriente: 01 IVA, 03 ICA, 04 INC. El catálogo trae además 02 IC, 05 ReteIVA, 06 ReteFuente, 07 ReteICA, 20 FtoHorticultura, 21 Timbre, 22 INC Bolsas, 23 INCarbono, 24 INCombustibles, 25 Sobretasa Combustibles, 26 Sordicom, 30 IC Datos, 32 ICL, 33 INPP, 34 IBUA, 35 ICUI, 36 ADV, ZZ Otro. No se valida contra el catálogo.
rate número Sí Porcentaje: 19, no 0.19. Se admite 0 para exento.

Solo 01 (IVA), 03 (ICA) y 04 (INC) entran por separado en el cálculo del CUFE. En el QR el reparto es otro: ValIva lleva solo el 01 y ValOtroIm agrupa todos los demás tributos, incluidos el ICA (03) y el INC (04).

Campo Tipo Obligatorio Descripción
form_code string Sí, si envías payment 1 contado, 2 crédito. Solo esos dos valores.
means_code string Sí, si envías payment Medio de pago, tabla 6.3.4.2: 1 instrumento no definido, 10 efectivo, 20 cheque, 31 transferencia débito, 42 consignación bancaria, 45 transferencia crédito bancario, 47 transferencia débito bancaria, 48 tarjeta crédito, 49 tarjeta débito, 71 bonos, 72 vales, ZZZ otro. No se valida contra el catálogo.
due_date string No Vencimiento en formato YYYY-MM-DD. Se devuelve también en el campo due_date del documento.

Si omites payment por completo, el servidor guarda { "formCode": "1", "meansCode": "10" } — contado, efectivo. No es un hueco: es un valor real que va al XML. Si vendes a crédito y no mandas payment, la factura sale diciendo que te pagaron de contado. El objeto es opcional; sus dos primeros campos, dentro de él, no.

Todo se recalcula en el servidor con aritmética decimal, a 2 decimales y redondeo HALF_UP. Por cada línea:

base = quantity × price
discountAmount = base × discount_rate / 100
lineExtension = base − discountAmount
taxAmount(t) = lineExtension × rate(t) / 100
totalAmount = lineExtension + Σ taxAmount

El impuesto se calcula sobre la base ya descontada, no sobre el precio de lista.

Y los totales del documento:

lineExtensionAmount = Σ lineExtension
taxExclusiveAmount = lineExtensionAmount (mismo valor)
taxInclusiveAmount = lineExtensionAmount + Σ impuesto por tributo
allowanceTotalAmount = "0.00" siempre
payableAmount = taxInclusiveAmount (mismo valor)

allowanceTotalAmount es siempre "0.00" porque los descuentos son de línea y ya están deducidos en lineExtensionAmount. No hay descuento a nivel de documento: para un 5 % sobre toda la venta, pon discount_rate: 5 en cada línea.

Un ejemplo que cuadra, con una línea de 2 × 10.000, 10 % de descuento e IVA del 19 %:

base 2 × 10.000 = 20.000,00
descuento 10 % −2.000,00
línea 18.000,00
IVA 19 % sobre 18.000 3.420,00
─────────────────────────────────────
total a pagar 21.420,00

taxTotals acumula por código de tributo: { "01": { "taxableAmount": "18000.00", "taxAmount": "3420.00" } }. Un tributo que no aparece en ninguna línea no aparece en el mapa — no llega en cero, no llega. Leer totals.taxTotals["04"] a ciegas en una factura sin INC da undefined.

Ventana de terminal
curl -X POST https://sandbox.facturacion.misofk.com/v1/invoices \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reference_code": "VE-2026-000841",
"numbering_range_id": "6f0a9c2e-4b1d-4a77-9f3e-2c8d5b71e004",
"customer": {
"identification_type": "31",
"identification": "901234567",
"dv": "8",
"person_type": "1",
"name": "Distribuidora El Poblado S.A.S.",
"tax_regime": "48",
"responsibilities": ["O-15"],
"email": "facturacion@elpoblado.com.co",
"phone": "6045550188",
"address": "Carrera 43A 5A-113 Oficina 502",
"municipality_code": "05001"
},
"items": [
{
"code": "SKU-001",
"name": "Camiseta algodón talla M",
"quantity": 2,
"unit_measure": "94",
"price": 45000,
"discount_rate": 10,
"taxes": [{ "code": "01", "rate": 19 }]
},
{
"code": "SKU-045",
"name": "Gorra bordada",
"quantity": 1,
"unit_measure": "94",
"price": 32000,
"taxes": [{ "code": "01", "rate": 19 }]
}
],
"payment": {
"form_code": "2",
"means_code": "45",
"due_date": "2026-10-04"
},
"notes": "Pedido 4471. Entrega en bodega Itagüí.",
"send_email": true
}'

La cuenta que sale de ahí:

línea 1 2 × 45.000 = 90.000 − 10 % = 81.000 IVA 15.390 total 96.390,00
línea 2 1 × 32.000 = 32.000 IVA 6.080 total 38.080,00
──────────────────────────────────────────────────────────────────────────────
subtotal 113.000,00
IVA 19 % 21.470,00
total a pagar 134.470,00

201 Created. Ojo: el 201 también sale en la repetición idempotente, que no crea nada.

{
"id": "b1f3a7d0-9c42-4e18-8a5b-6d0e2f4c9a11",
"type": "01",
"reference_code": "VE-2026-000841",
"full_number": null,
"prefix": "SETP",
"number": null,
"status": "QUEUED",
"cufe": null,
"qr_url": null,
"qr_content": null,
"issue_date": null,
"issue_time": null,
"due_date": "2026-10-04",
"operation_type": "10",
"currency": "COP",
"customer": {
"identificationType": "31",
"identification": "901234567",
"dv": "8",
"personType": "1",
"name": "Distribuidora El Poblado S.A.S.",
"taxRegime": "48",
"responsibilities": ["O-15"],
"email": "facturacion@elpoblado.com.co",
"phone": "6045550188",
"address": "Carrera 43A 5A-113 Oficina 502",
"municipalityCode": "05001"
},
"items": [
{
"code": "SKU-001",
"name": "Camiseta algodón talla M",
"quantity": "2",
"unitMeasure": "94",
"price": "45000.00",
"discountRate": "10.00",
"discountAmount": "9000.00",
"lineExtensionAmount": "81000.00",
"taxes": [
{ "code": "01", "rate": "19.00", "taxableAmount": "81000.00", "taxAmount": "15390.00" }
],
"totalAmount": "96390.00"
},
{
"code": "SKU-045",
"name": "Gorra bordada",
"quantity": "1",
"unitMeasure": "94",
"price": "32000.00",
"discountRate": "0.00",
"discountAmount": "0.00",
"lineExtensionAmount": "32000.00",
"taxes": [
{ "code": "01", "rate": "19.00", "taxableAmount": "32000.00", "taxAmount": "6080.00" }
],
"totalAmount": "38080.00"
}
],
"totals": {
"lineExtensionAmount": "113000.00",
"taxExclusiveAmount": "113000.00",
"taxInclusiveAmount": "134470.00",
"allowanceTotalAmount": "0.00",
"payableAmount": "134470.00",
"taxTotals": { "01": { "taxableAmount": "113000.00", "taxAmount": "21470.00" } }
},
"payment": { "formCode": "2", "meansCode": "45", "dueDate": "2026-10-04" },
"notes": "Pedido 4471. Entrega en bodega Itagüí.",
"dian_status_code": null,
"dian_errors": null,
"related_document_id": null,
"environment": "HABILITACION",
"email_sent_at": null,
"email_to": null,
"email_error": null,
"created_at": "2026-09-04T14:22:31.004Z"
}
Campo Tipo Descripción
id string (UUID) El identificador del documento en este API. Es el de GET /v1/documents/:id, y el que va como related_document_id de una nota.
type string Tipo DIAN. En este endpoint, siempre 01.
reference_code string El que enviaste.
full_number string | null Prefijo + consecutivo, SETP990000001. null hasta que se numera.
prefix string Prefijo del rango. Cadena vacía si el rango no tiene.
number string | null El consecutivo. Llega como cadena, no como número. null hasta numerar.
status string Al crear, siempre QUEUED. Ver ciclo de vida.
cufe string | null CUFE en hexadecimal SHA-384, 96 caracteres. null hasta firmar.
qr_url string | null URL del catálogo de la DIAN donde se consulta el documento.
qr_content string | null Lo que va dentro del QR, tal cual se firmó. Es esto lo que hay que codificar, no qr_url: un QR que solo lleve la URL no cumple el anexo técnico.
qr_image string Data URI de un PNG de 300 px. Solo en GET /v1/documents/:id; no en la creación ni en el listado.
issue_date string | null YYYY-MM-DD en hora legal de Colombia. Lo pone el servidor al numerar.
issue_time string | null HH:mm:ss-05:00.
due_date string | null El payment.due_date que enviaste.
operation_type string Lo pone el servidor: 10 para una factura de venta. No es un campo de entrada.
currency string Siempre COP. Este API no emite en otra moneda.
customer objeto Copia del adquirente en el momento de emitir, en camelCase: identificationType, personType, taxRegime, municipalityCode.
items arreglo El detalle recalculado por el servidor, no el que enviaste. Cada línea: code, name, quantity, unitMeasure, price, discountRate, discountAmount, lineExtensionAmount, taxes[], totalAmount. Cada impuesto: code, rate, taxableAmount, taxAmount.
totals objeto lineExtensionAmount, taxExclusiveAmount, taxInclusiveAmount, allowanceTotalAmount, payableAmount y taxTotals.
payment objeto | null En camelCase: formCode, meansCode, dueDate.
notes string | null La observación que enviaste.
dian_status_code string | null Lo que devolvió la DIAN al transmitir.
dian_errors arreglo | null [{ rule?, message }]. Trae los motivos del rechazo de la DIAN, y también el motivo de un fallo interno al construir el documento.
related_document_id string | null Solo en notas. En una factura, null.
environment string HABILITACION o PRODUCCION. Es el del emisor, no el de tu cuenta.
email_sent_at fecha | null Cuándo salió el correo.
email_to string | null A dónde salió.
email_error string | null Por qué no salió. Por ejemplo, El adquirente no tiene correo registrado.
created_at fecha Cuándo se aceptó la factura.

Porque la emisión es asíncrona, y eso es justo lo que hace que el API responda rápido y sobreviva a que la DIAN se caiga.

Lo que ocurre dentro de la petición es corto: se valida el cuerpo, se comprueban el emisor y el rango, se descuenta cupo del plan, se recalculan los montos y se guarda la factura en QUEUED. Ahí termina el 201. Tomar el consecutivo, armar el UBL, calcular el CUFE, firmarlo con el certificado y transmitirlo a la DIAN pasa después, en segundo plano.

Así que el 201 significa «aceptada y encolada», no «validada por la DIAN». full_number, number, cufe, qr_url, qr_content, issue_date e issue_time se llenan cuando el documento se numera y se firma; dian_status_code, cuando la DIAN contesta.

Para enterarte del resultado, lo correcto es un webhook — document.validated, document.rejected— y no consultar en bucle. El detalle de cada estado, y cómo se recupera un documento parado, está en el ciclo de vida.

Tampoco vuelven en la respuesta, aunque los hayas enviado o existan en el documento: numbering_range_id, send_email, discarded_at, discard_reason y dian_track_id.

Todos siguen la forma común. Van en el orden en que se comprueban: responde el primero que falle.

HTTP code Cuándo
401 UNAUTHORIZED API key inválida o ausente — falta la cabecera, no es Bearer, o el token venció.
403 FORBIDDEN Esa credencial solo sirve para dar de alta emisores.
403 FORBIDDEN Tu cuenta no tiene emisor asignado
400 VALIDATION Un campo falta, sobra o no cumple. message trae el primero y errors[] todos: property total should not exist, reference_code must be shorter than or equal to 100 characters, customer should not be null or undefined.
409 IDEMPOTENCY_CONFLICT reference_code {valor} ya existe con otro tipo de documento — ese código lo usaste en una nota, un soporte o un POS.
422 VALIDATION El emisor {nombre} está suspendido y no puede emitir documentos.
404 NOT_FOUND Rango de numeración no encontrado — el UUID no existe, o el rango es de otro emisor.
422 VALIDATION El rango {uuid} es de tipo {CREDIT_NOTE}, no aplica para este documento — mandaste a /v1/invoices un rango que no es INVOICE.
402 PLAN_REQUIRED La empresa no tiene un paquete de documentos contratado
402 PLAN_EXPIRED El paquete "{nombre}" venció el {fecha}. Renuévalo para seguir emitiendo.
402 PLAN_EXHAUSTED Se agotaron los {N} documentos del paquete "{nombre}". Contrata una recarga para seguir emitiendo.
Los tres objetos que el cuerpo da por descontados —customer, items y
cada elemento de items— se comprueban por presencia, no solo por
contenido: omitir la clave entera devuelve 400 nombrando el campo, igual
que mandarla vacía o en null. payment es el único opcional de los
cuatro.

Dos cosas del cupo del plan que importan al integrar: se descuenta antes de guardar la factura, y se devuelve si el guardado falla; y una repetición idempotente no consume cupo, porque sale antes de llegar a esa comprobación.

Lo que no vas a ver aquí es un error de la DIAN. Cuando responde el 201, el documento ni siquiera se ha firmado. Un rechazo de la DIAN llega después, en el estado REJECTED y en dian_errors.

  • Ciclo de vida del documento — qué pasa entre QUEUED y VALIDATED, y cómo se recupera un documento parado
  • Rangos de numeración — de dónde sale el numbering_range_id y por qué hace falta la clave técnica
  • Errores — la lista completa de códigos y cuáles se reintentan