Emitir una factura
Emitir es un POST a /v1/companies/{company_id}/documents. La solicitud espera el resultado de la DIAN y responde con el documento ya validado: 201 con accepted. Si la DIAN lo rechaza responde 200, y si no alcanza a terminar, 202 (qué responde la emisión). Los ejemplos usan el sandbox; con otra clave son idénticos. Los valores exactos de cada campo están en la referencia de campos y el contrato en la API v1.
Lo que necesitas
Sección titulada «Lo que necesitas»- Una clave con permiso
documents:writey elcompany_id(ambientes y claves). - Una
resolution_id(numeración). En el sandbox es18760000000000. - Una
Idempotency-Keypor venta (idempotencia).
Factura a un comprador identificado
Sección titulada «Factura a un comprador identificado»curl -s -X POST "https://api.foliios.com/v1/companies/$FOLIIOS_COMPANY_ID/documents" \ -H "Authorization: Bearer $FOLIIOS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: venta-1042" \ -d '{ "resolution_id": "18760000000000", "type": "invoice", "customer": { "identification": {"type": "nit", "number": "900373076"}, "name": "Comercializadora Andina S.A.S.", "emails": ["facturas@andina.example"] }, "lines": [{ "code": "PARQ-HORA", "description": "Parqueadero por horas", "quantity": "3", "unit": "HUR", "unit_price": "4000.00", "taxes": [{"scheme": "iva", "percent": "19"}] }], "payment": {"form": "cash", "means": "10"}, "notes": ["Gracias por su compra"] }'Lo mismo en Python, con el helper llamar del inicio rápido:
from foliios_inicio import EMPRESA, llamar
estado, documento, _ = llamar( "POST", f"/v1/companies/{EMPRESA}/documents", { "resolution_id": "18760000000000", "type": "invoice", "customer": { "identification": {"type": "nit", "number": "900373076"}, "name": "Comercializadora Andina S.A.S.", "emails": ["facturas@andina.example"], }, "lines": [ { "code": "PARQ-HORA", "description": "Parqueadero por horas", "quantity": "3", "unit": "HUR", "unit_price": "4000.00", "taxes": [{"scheme": "iva", "percent": "19"}], } ], }, {"Idempotency-Key": "venta-1042"},)assert estado in (200, 201, 202), documentoprint(estado, documento["id"], documento["status"]) # 201 acceptedQué responde la emisión
Sección titulada «Qué responde la emisión»La solicitud espera el resultado de la DIAN. El código dice cómo terminó:
| Respuesta | El documento quedó | Qué trae | Qué hacer |
|---|---|---|---|
201 |
accepted, o contingency |
El documento con number y cufe; print cuando está disponible. |
Guardarlo. No hace falta consultar. |
200 |
rejected o failed |
El documento con el detalle en dian. |
Corregir y reenviar, o corregir la causa del fallo. |
202 |
Sigue en proceso (pending, sending, dian_unavailable) |
El mismo cuerpo que GET, la cabecera Location y, si hubo espera, Retry-After. |
Consultar con GET o esperar el webhook. No emitir de nuevo. |
Las tres llevan Location con la URL del documento.
El bloque print
Sección titulada «El bloque print»El campo print de un documento accepted o en contingency reúne lo que hace falta para imprimirlo sin otra consulta: cufe (CUFE o CUDE), qr_text y qr_url para el código QR, validated_at (cuándo lo validó la DIAN; null en contingencia), status_label (la leyenda que va bajo el QR), software y, en facturas, numbering_authorization.
Dentro de software, name y manufacturer_name son el software y su fabricante tal como los imprime la representación gráfica del mismo documento, y manufacturer_id es el NIT del fabricante: la clave está siempre y vale null cuando el documento no declara ninguno. Son los que tu empresa declaró en el portal (Software y fabricante) o, si no declaró un fabricante propio, los de foliios. Esos datos quedan fijos cuando el documento se emite: consultarlo después devuelve los mismos, aunque tu empresa cambie su configuración.
El campo print es opcional: es null en cualquier otro estado y también puede venir null en un documento aceptado, cuando el ambiente no lo tiene disponible. Tu programa debe tolerar ese null y, en ese caso, usar number y cufe del documento. La forma exacta está en la API v1.
Si imprimes la factura en un punto de venta, la guía tirilla en un punto de venta dice qué va en la tirilla, cuándo puedes contar con print y qué imprimir ante un 202 o un 429.
Cuánto espera: ?wait=N
Sección titulada «Cuánto espera: ?wait=N»wait es el número de segundos que la solicitud espera el resultado: un entero, 0 o más.
- Sin
waitaplica el valor por defecto del ambiente. ?wait=0no espera: responde202de inmediato con el documento enpending, sinRetry-After.- Un valor mayor al tope (23 segundos) se recorta al tope.
- Un valor que no es un entero de 0 en adelante (
abc,-1,1.5, vacío o repetido) responde400 invalid_query, conwaitenerrors[].
El tiempo de espera de tu cliente HTTP debe ser mayor que wait. Las solicitudes que esperan ocupan un cupo por empresa (límites); con ?wait=0 no lo ocupan.
curl -s -i -X POST "https://api.foliios.com/v1/companies/$FOLIIOS_COMPANY_ID/documents?wait=0" \ -H "Authorization: Bearer $FOLIIOS_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: venta-1044" -d @factura.json# HTTP/2 202, con "status": "pending"Si la respuesta trae dian_outcome_uncertain
Sección titulada «Si la respuesta trae dian_outcome_uncertain»Un 202 puede traer outcome_code: dian_outcome_uncertain y un outcome_message: la DIAN no respondió a tiempo y puede tener el documento. No lo retransmitas con otra Idempotency-Key: podrías emitir dos documentos por una venta. foliios verifica el resultado antes de enviar nada más. Consulta el documento con GET (la URL de Location) hasta que quede en un estado final.
Reglas que conviene saber
Sección titulada «Reglas que conviene saber»- Montos, porcentajes y cantidades van como texto decimal (
"4000.00","19"), nunca como números JSON. Un número en esos campos responde400 invalid_request. - Campos desconocidos se rechazan (
400 invalid_request): un error de tipeo no pasa en silencio. - Por defecto, el pago es de contado con medio
10(efectivo), la monedaCOPy la unidad94(unidad). lines[].code(tu código de producto) es opcional.- Impuestos: gravado con exactamente uno de
percentoper_unit, ocategoryexempt/excludedsin tarifa. rounding(opcional) permite redondear el total a pagar para cobrar sin centavos.
Factura a consumidor final
Sección titulada «Factura a consumidor final»Si omites customer, la factura se emite al consumidor final anónimo de la DIAN. Es lo habitual en una venta de mostrador que no pide factura a nombre de nadie.
curl -s -X POST "https://api.foliios.com/v1/companies/$FOLIIOS_COMPANY_ID/documents" \ -H "Authorization: Bearer $FOLIIOS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: venta-1043" \ -d '{ "resolution_id": "18760000000000", "type": "invoice", "lines": [{"description": "Parqueadero por horas", "quantity": "1", "unit_price": "4000.00"}] }'Sin correo del comprador, la entrega queda not_applicable: el comprador recibe la representación gráfica impresa. Puedes descargarla (ver entrega por correo) o armar tu propia tirilla con print (ver tirilla en un punto de venta).
Si el comprador sí pide la factura a su nombre
Sección titulada «Si el comprador sí pide la factura a su nombre»Basta este customer:
{"identification": {"type": "citizenship_card", "number": "1020304050"}}El nombre o el correo que falten se consultan en la DIAN antes de armar el documento. Lo que tú envías siempre gana. Si no hay nombre ni tuyo ni de la DIAN, la respuesta es 422 validation_failed con customer.name / name_required. Ver el campo customer en la referencia.
Consultar el resultado
Sección titulada «Consultar el resultado»Después de un 202, o en cualquier momento, el documento se consulta con GET:
curl -s -H "Authorization: Bearer $FOLIIOS_API_KEY" \ "https://api.foliios.com/v1/companies/$FOLIIOS_COMPANY_ID/documents/$DOCUMENT_ID"import time
for _ in range(30): if documento["status"] in ("accepted", "rejected", "failed"): break time.sleep(2) _, documento, _ = llamar("GET", f"/v1/companies/{EMPRESA}/documents/{documento['id']}")print(documento["status"], documento["number"], documento["cufe"])Cuando hay webhooks, no hace falta consultar en bucle: foliios avisa. Para ver la lista de documentos, GET .../documents con status, created_from, created_to y limit (de 1 a 100); la respuesta trae data, has_more y next_cursor. Una página filtrada por estado puede traer menos filas que limit y aun así has_more: true: sigue el cursor hasta que sea false.
Descargas
Sección titulada «Descargas»| Qué | Ruta |
|---|---|
| XML firmado | GET .../documents/{document_id}/xml |
| Representación gráfica (PDF) | GET .../documents/{document_id}/pdf |
El contenedor entregado (ZIP, o ?format=xml) |
GET .../documents/{document_id}/attached-document |
Antes de que exista el archivo, el XML responde 409 xml_not_available y el PDF 409 pdf_not_available.
Errores frecuentes al emitir
Sección titulada «Errores frecuentes al emitir»| Respuesta | Qué pasa | Qué hacer |
|---|---|---|
400 missing_idempotency_key |
Falta la cabecera. | Envíala. |
400 invalid_query |
wait no es un entero de 0 en adelante. |
Corrige el parámetro. |
422 validation_failed |
El documento rompe una regla. errors[] lista todos los campos con su ruta. |
Corrige cada campo y reenvía con la misma Idempotency-Key: un error que no consume nada libera la clave. |
422 resolution_expired / resolution_not_found |
La resolución venció o no existe. | Revisa la resolución en el portal. |
409 environment_not_configured |
La empresa no está configurada en ese ambiente, o le falta una pieza de la configuración (el detail dice cuál). |
Completa el alta de ese ambiente y repite con la misma Idempotency-Key. |
409 habilitation_required |
Con una clave sk_test_, la empresa no aprobó el set. |
Ver habilitación. |
429 |
Límite de frecuencia o de la empresa (rate_limited, in_flight_limited, concurrency_limited). |
Espera Retry-After y reintenta (reintentos, límites). |
El catálogo completo está en errores.