Emitir una factura
Emitir es un POST a /v1/companies/{company_id}/documents. La respuesta es 202 con el documento en pending: la DIAN lo valida después, de forma asíncrona. 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 == 202, documentoprint(documento["id"], documento["status"]) # pendingReglas 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.- El correo de un comprador facturador electrónico debe ser el que registró en la DIAN o el medio que él haya señalado.
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).
Si el comprador sí pide la factura a su nombre
Sección titulada «Si el comprador sí pide la factura a su nombre»Solo se le puede pedir tipo y número de documento, nombre y correo. 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 la norma y el campo customer en la referencia.
Consultar el resultado
Sección titulada «Consultar el resultado»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): _, documento, _ = llamar("GET", f"/v1/companies/{EMPRESA}/documents/{documento['id']}") if documento["status"] in ("accepted", "rejected", "failed"): break time.sleep(2)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. |
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. | Completa el alta de ese ambiente. |
409 habilitation_required |
Con una clave sk_test_, la empresa no aprobó el set. |
Ver habilitación. |
429 |
Límite de frecuencia. | Espera Retry-After y reintenta (reintentos). |
El catálogo completo está en errores.