Ir al contenido

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.

Ventana de terminal
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, documento
print(documento["id"], documento["status"]) # pending
  • Montos, porcentajes y cantidades van como texto decimal ("4000.00", "19"), nunca como números JSON. Un número en esos campos responde 400 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 moneda COP y la unidad 94 (unidad).
  • lines[].code (tu código de producto) es opcional.
  • Impuestos: gravado con exactamente uno de percent o per_unit, o category exempt / excluded sin 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.

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.

Ventana de terminal
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.

Ventana de terminal
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.

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.

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.