# Tu primera factura electrónica por API en 15 minutos (sandbox gratis)

> Tutorial paso a paso: llave API, sandbox, el JSON mínimo de la factura, la respuesta con CUFE, webhooks de aceptación, notas crédito e idempotencia.

Fuente: https://miplata.co/blog/primera-factura-por-api · Publicado: 2026-10-10 · Actualizado: 2026-10-23 · Autor: Equipo miPlata · Categoría: API e integraciones
Producto: miPlata FACTURAR (https://facturar.miplata.co/api-docs) · Contacto: hola@miplata.co

## Antes de empezar

- La API vive en **tu propia instancia**: `https://<tu-nit>.miplata.co/api/external/…`. No hay un host compartido ni un proveedor intermedio; tu empresa factura con su Software ID y su certificado.
- **Sandbox** es un interruptor de toda la instancia (Configuración → Entorno), la DIAN de habilitación y las pasarelas de prueba. Misma llave, mismas rutas, sin consumir documentos. Es donde haremos todo este tutorial.
- La documentación tiene cada endpoint con ejemplos en cURL, Node.js y Python, además de versiones en texto plano (`/api-docs.md`) y JSON para tu asistente de código, y una colección Postman.

![Configuración → Claves API: cada llave con sus IP permitidas, límite por minuto, uso diario y vencimiento](https://miplata.co/blog/app/real-api-01-claves.webp)
## Paso a paso

### Crea una llave API
En tu cuenta, **Configuración → Integraciones → Claves API → Crear clave API**: nombre (por ejemplo «ERP»), IP o rango permitido (obligatorio) y límite por minuto. La llave se muestra **una sola vez**; guárdala en tu gestor de secretos. Puedes crear tokens personales (PAT) por integración o colaborador desde la misma llave sin repartir la raíz.

### Pon la instancia en Sandbox
**Configuración → Entorno → Sandbox**. Los documentos irán a la DIAN de habilitación, sin validez fiscal ni consumo de cupo. `GET /company` te lo confirma en `mode: "TEST"` y cada documento lleva `environment`. En Sandbox hay además un simulador de caída de la DIAN para probar tus reintentos y webhooks.

```bash
curl -sS https://TU-EMPRESA.miplata.co/api/external/company \
  -H "X-API-Key: TU_LLAVE"
```

### Emite la factura
Un `POST /invoices` con el cliente y las líneas. En la primera emisión a un NIT envías sus datos; en las siguientes basta `customer_nit` e `items`, el resto se toma de su ficha. El IVA va por línea en `tax_percent`.

```bash
curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORD-1001" \
  -d '{
  "customer_nit": "900123456",
  "customer_name": "Acme SAS",
  "customer_type_doc": "31",
  "customer_person_type": "1",
  "customer_tax_regime": "48",
  "customer_email": "pagos@acme.co",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "payment_means_code": "10",
  "order_reference": "ORD-1001",
  "items": [
    { "description": "Consultoría", "quantity": 10, "unit_price": 150000, "tax_percent": 19 }
  ]
}'
```

La respuesta (`201`) trae el documento ya numerado, firmado y enviado a la DIAN. El consecutivo lo pone miPlata: no lo mandas tú. El `id` es un UUID; guárdalo para consultar, cobrar o corregir:

```json
{
  "id": "3f2b8c1e-7a4d-4b9e-9c61-2d5f0a8e4b17",
  "environment": "TEST",
  "invoice_number": "SETP990000123",
  "invoice_type": "01",
  "cufe": "a1b2c3d4e5f6...9f",
  "qr_code": "https://catalogo-vpfe-hab.dian.gov.co/document/searchqr?documentkey=a1b2...",
  "dian_status": "SENT",
  "total_amount": "1785000.00",
  "balance": "1785000.00"
}
```

### Espera la aceptación
`dian_status` llega como `SENT` (enviada, esperando la respuesta) o ya como `ACCEPTED`; si la DIAN no responde, `QUEUED` y miPlata reintenta sola. La DIAN responde en segundos. Dos formas de saberlo:

- **Webhook**: registra una URL con `POST /webhooks` (eventos `invoice.accepted`, `invoice.rejected`, `invoice.pending_dian`; las notas crédito disparan `credit_note.*`); cada entrega va firmada con HMAC-SHA256 en `X-MiPlata-Signature` (y `X-MiPlata-Signature-V2` sobre `X-MiPlata-Timestamp` + cuerpo).
- **Consulta**: `GET /invoices/{id}` hasta que `dian_status` sea `ACCEPTED` (o `REJECTED`, con el código de regla y su explicación).

```bash
curl -sS https://TU-EMPRESA.miplata.co/api/external/invoices/3f2b8c1e-7a4d-4b9e-9c61-2d5f0a8e4b17 \
  -H "X-API-Key: TU_LLAVE"
```

### Descarga el PDF y el XML
`GET /invoices/{id}/pdf` devuelve la representación gráfica con QR; `GET /invoices/{id}/attached-document` el AttachedDocument firmado (XML + ApplicationResponse de la DIAN) que el cliente puede exigir.

### Corrige con una nota crédito
Una factura aceptada no se edita: se emite una nota crédito (`invoice_type: "91"`) que referencia el original. Sus webhooks son de la familia `credit_note.*` (`credit_note.accepted`, `credit_note.rejected`…), no `invoice.*`.

```bash
curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{
  "invoice_type": "91",
  "correction_invoice_id": "3f2b8c1e-7a4d-4b9e-9c61-2d5f0a8e4b17",
  "correction_concept": "2",
  "correction_reason": "Anulación de la factura",
  "customer_nit": "900123456",
  "items": [
    { "description": "Consultoría", "quantity": 10, "unit_price": 150000, "tax_percent": 19 }
  ]
}'
```

### Pasa a producción
**Configuración → Entorno → Producción**. Nada cambia en tu código: misma llave, mismas rutas. Desde ahí cada documento transmitido a la DIAN consume una unidad del cupo del plan (1.000 en el Plan 12 Meses, 250 en el de 3) y las recargas cubren el resto.

## Tres cosas que evitan dolores de cabeza

- **Idempotencia.** Manda `Idempotency-Key` (tu número de pedido) en cada `POST /invoices`: si tu sistema reintenta con el mismo cuerpo dentro de 24 horas, recibes `200` con `Idempotent-Replayed: true` y el mismo documento, en vez de una segunda factura con otro consecutivo. Un `order_reference` repetido en facturas de venta (sin esa llave o pasadas las 24 horas) responde `409` con el id existente.
- **DIAN caída.** Cuando la DIAN no responde, el documento queda `QUEUED` ya firmado y numerado; miPlata reintenta (3 × 20 s, luego cada 30 minutos, 48 horas) y avisa por `invoice.pending_dian` y luego `invoice.accepted`. No reintentes el `POST`.
- **Rechazos.** `invoice.rejected` trae la regla (por ejemplo `FAU14`: el total a pagar no cuadra; `FAJ43b`: el nombre del emisor no coincide con el RUT) con causa y corrección en `errors_explained`. Corriges con `PUT /invoices/{id}` (solo los campos que cambian, más `"finalize": true, "send_to_dian": true`): mismo número, nuevo CUFE, reenvío en la misma llamada. `POST /invoices/{id}/retry-now` no sirve para esto: solo transmite documentos en cola por caída de la DIAN. Guía: [factura rechazada por la DIAN](/blog/factura-electronica-rechazada-dian).

![Configuración → Webhooks: URL, eventos suscritos y último estado de entrega; el secreto se muestra una sola vez](https://miplata.co/blog/app/real-api-02-webhooks.webp)
## Preguntas frecuentes

#### ¿Hay un entorno de pruebas separado con otra URL?
No. Sandbox es un estado de toda la instancia, no un host ni una llave distinta. Eso evita el error clásico de apuntar producción al sandbox: cambias el interruptor y listo.

#### ¿Qué pasa si envío un cliente que ya existe con datos distintos?
Los campos que envíes actualizan su ficha; los que omitas se conservan. Si solo mandas `customer_nit`, se usa la ficha tal como está.

#### ¿Puedo emitir muchas facturas de golpe?
Sí: `POST /invoices/bulk` acepta un lote con `client_reference` por ítem y `GET /invoices/bulk/{batch_id}` devuelve el estado de cada una.

#### ¿Cómo cobro la factura desde la API?
`POST /invoices/{id}/pay-link` genera el enlace de pago (Bre-B, Nequi, Daviplata o PayPal); el webhook `invoice.paid` avisa cuando el cliente paga. Guía: [cobrar con Bre-B y Nequi](/blog/cobrar-facturas-bre-b-nequi-daviplata).

#### ¿Y la nómina y el documento soporte?
Mismo esquema: `POST /payroll` para documentos 102/103 e `invoice_type: "05"` / `"95"` para documento soporte y su nota de ajuste. Todo en la documentación.

#### ¿Qué lenguaje debo usar?
Cualquiera con cliente HTTP y JSON. La documentación muestra cURL, Node.js y Python; la colección Postman cubre todos los endpoints públicos.
