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/invoiceshttps://qa.facturacion.misofk.com/v1/invoiceshttps://facturacion.misofk.com/v1/invoicesCabeceras
Sección titulada «Cabeceras»Authorization: Bearer <access_token>Content-Type: application/jsonSolo 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ú. |
customer — el adquirente
Sección titulada «customer — el adquirente»| 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.
items[] — las líneas
Sección titulada «items[] — las líneas»| 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. |
items[].taxes[]
Sección titulada «items[].taxes[]»| 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).
payment
Sección titulada «payment»| 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.
Cómo se calculan los montos
Sección titulada «Cómo se calculan los montos»Todo se recalcula en el servidor con aritmética decimal, a 2 decimales y
redondeo HALF_UP. Por cada línea:
base = quantity × pricediscountAmount = base × discount_rate / 100lineExtension = base − discountAmounttaxAmount(t) = lineExtension × rate(t) / 100totalAmount = lineExtension + Σ taxAmountEl impuesto se calcula sobre la base ya descontada, no sobre el precio de lista.
Y los totales del documento:
lineExtensionAmount = Σ lineExtensiontaxExclusiveAmount = lineExtensionAmount (mismo valor)taxInclusiveAmount = lineExtensionAmount + Σ impuesto por tributoallowanceTotalAmount = "0.00" siemprepayableAmount = 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,00descuento 10 % −2.000,00línea 18.000,00IVA 19 % sobre 18.000 3.420,00─────────────────────────────────────total a pagar 21.420,00taxTotals 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.
Ejemplo de petición
Sección titulada «Ejemplo de petición»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,00línea 2 1 × 32.000 = 32.000 IVA 6.080 total 38.080,00──────────────────────────────────────────────────────────────────────────────subtotal 113.000,00IVA 19 % 21.470,00total a pagar 134.470,00Respuesta
Sección titulada «Respuesta»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"}Campos de la respuesta
Sección titulada «Campos de la respuesta»| 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. |
Por qué full_number y cufe llegan en null
Sección titulada «Por qué full_number y cufe llegan en null»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.
Errores
Sección titulada «Errores»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.
Después de esto
Sección titulada «Después de esto»- Ciclo de vida del documento — qué pasa entre
QUEUEDyVALIDATED, y cómo se recupera un documento parado - Rangos de numeración — de dónde sale el
numbering_range_idy por qué hace falta la clave técnica - Errores — la lista completa de códigos y cuáles se reintentan