miPlata FACTURAR
Documentación de la API

API de documentos

Integre miPlata FACTURAR con su sistema. Versión 1.20 · Actualizado 23 de octubre de 2026.

¿Prefiere que lo integremos nosotros? Conectamos su POS, ERP o tienda con precio fijo. Ver paquetes de integración →

Introducción

Descripción

La API de documentos permite que su sistema (un ERP, un sistema de gestión o cualquier otra aplicación) emita y consulte documentos electrónicos en su instancia de miPlata FACTURAR. Se trata de la misma instancia que opera desde el navegador: los documentos creados por API se reflejan en la aplicación, y a la inversa.

Cubre los cuatro grupos de documento electrónico: factura de venta (01) y de exportación (02), con sus notas crédito (91) y débito (92); documento soporte en adquisiciones efectuadas a sujetos no obligados a facturar (05), con su nota de ajuste (95); y nómina electrónica (102), con su nota de ajuste (103).

Los documentos se generan, firman y transmiten a la DIAN desde su instancia, con el Software ID, la resolución de numeración y el certificado de firma digital de la empresa. La API no altera ese modelo: es un canal adicional sobre el mismo motor de la aplicación web, de modo que la validación, la numeración (CUFE, CUDE y CUDS) y el transporte son idénticos.

Activación

La API está incluida en el plan y se entrega deshabilitada: no responde hasta que se cree una llave. Mientras no exista una llave activa, toda solicitud a /api/external es rechazada.

Para crear una llave, ingrese a su instancia → Configuración → Claves API. El secreto se exhibe una sola vez, en el momento de la creación. En el servidor se almacena únicamente su resumen criptográfico (SHA-256); no es posible recuperarlo ni exhibirlo de nuevo. Si lo pierde, regenere la llave.

  • Nombre: identifica el sistema que utiliza la llave.
  • Vencimiento (opcional): 30, 90, 180 o 365 días. Al vencer, deja de autenticar, aunque permanezca marcada como activa.
  • Límite de tasa: 60 solicitudes por minuto, valor predeterminado, ajustable por llave hasta el máximo de la instancia.
  • Direcciones IP permitidas (obligatorio): IP públicas o rangos CIDR del servidor que llama a la API, separados por coma. La llave autentica únicamente desde esas direcciones; una llave sin IP no puede crearse y, si existiera, es rechazada con 403. Se compara con la IP real que recibe el borde de la red, por lo que debe ser una IP pública fija (servidor, ERP en la nube, NAT de salida), no la de una conexión doméstica que cambia. Solo miPlata puede levantar este requisito para una instancia.

Sandbox y producción

El entorno es un único control de toda la instancia, en Configuración → Entorno (Sandbox / Producción); no es un parámetro por solicitud. No hay otro dominio, otra llave ni otro encabezado para pruebas: la misma llave y las mismas rutas operan en el entorno activo. GET /company lo informa en mode (TEST o PRODUCTION) y cada documento en environment.

Sandbox cambia a la vez la DIAN y los cobros. Los documentos van a la DIAN en habilitación (sin validez fiscal, rango de pruebas compartido, sin consumo de cupo ni 402) y los cobros usan la tienda WAVA de pruebas y PayPal sandbox: no se mueve dinero real. Los enlaces de pago y los webhooks de pago indican rail: sandbox, y un pago de prueba nunca liquida una factura de producción. Es el modo previsto para probar el ciclo completo —cobro, factura, webhooks— antes de producción.

Las consultas de la API y la bandeja muestran solo documentos del entorno vigente: los de producción quedan ocultos mientras Sandbox está activo, y a la inversa. Volver a producción exige la vinculación de producción ante la DIAN verificada.

Simulador de caída de la DIAN (solo Sandbox, en Configuración → Entorno): cada transmisión falla como si la DIAN no respondiera; el documento queda QUEUED con dian_queue, se emite invoice.pending_dian y, al desactivarlo, se transmite de verdad y pasa a ACCEPTED (invoice.accepted). GET /company expone simulate_dian_outage. Fuera de Sandbox el interruptor responde 409.

  • Sandbox = DIAN habilitación + WAVA de pruebas + PayPal sandbox; misma llave, mismas rutas.
  • mode en GET /company, environment en cada documento, rail en cada enlace de pago y webhook de pago.
  • Simulador de caída: prueba QUEUED → invoice.pending_dian → ACCEPTED, «Reintentar ahora» (POST /invoices/{id}/retry-now) y tipo 04 (POST /invoices/{id}/contingency-04).

Autenticación

Autenticación

Envíe el secreto en el encabezado HTTP X-API-Key. Únicamente se admite HTTPS; la llave no se acepta en la cadena de consulta ni en el cuerpo, a fin de que no quede registrada en bitácoras ni en el historial del navegador.

La llave de API no es una credencial de la aplicación web: autoriza exclusivamente /api/external/*. No habilita el inicio de sesión, el acceso a la interfaz, la administración de usuarios ni la creación de otras llaves.

Encabezado en cada solicitud

X-API-Key: TU_LLAVE

Tokens de acceso personal (PAT)

A partir de su llave puede emitir tokens de acceso personal, sin intervención de un administrador: uno por cada proceso de integración continua, por cada equipo o por cada colaborador. Se presentan en el mismo encabezado X-API-Key y autorizan exactamente las mismas rutas /api/external/*.

Un token hereda —sin posibilidad de ampliarlos— el límite de tasa y la lista de direcciones IP de la llave que lo originó, y vence en un plazo máximo de 90 días (nunca posterior al de su llave de origen). La lista de IP se lee de la llave de origen en cada solicitud: si cambia las IP de la llave, sus tokens siguen la nueva lista de inmediato. El secreto se exhibe una sola vez, en el momento de la creación; en el servidor se almacena únicamente su resumen criptográfico SHA-256.

Son secretos opacos, no JWT: el contenido no es legible, no existe una clave de firma que deba rotarse, y la revocación es inmediata. DELETE /tokens/{id} lo revoca de inmediato; revocar o regenerar la llave de origen revoca en cascada todos sus tokens (la respuesta informa revoked_child_tokens). Un token no puede crear ni administrar otros tokens, y únicamente consulta o elimina los que él mismo creó.

Cuerpo de POST /tokens

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/tokens \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "ci-deploy",
  "expires_in_days": 30
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/tokens", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "name": "ci-deploy",
  "expires_in_days": 30
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "name": "ci-deploy",
  "expires_in_days": 30
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/tokens",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/tokens");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "name" => "ci-deploy",
    "expires_in_days" => 30
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201 — el secreto se exhibe una sola vez

{
  "id": "key_01H9A...",
  "name": "ci-deploy",
  "created_at": "2026-09-19T14:03:22Z",
  "expires_at": "2026-10-19T14:03:22Z",
  "last_used": null,
  "secret": "el-secreto-se-exhibe-una-sola-vez"
}

URL base

Cada cliente dispone de su propio dominio. Sustituya TU-EMPRESA por el subdominio de su instancia (el mismo con el que ingresa a la aplicación). Todas las rutas de los ejemplos se construyen a partir de esta URL.

https://TU-EMPRESA.miplata.co/api/external

Referencia

Endpoints

Todas las rutas se construyen a partir de la URL base. Las respuestas se entregan en JSON, salvo los recursos /pdf, que devuelven application/pdf. Las solicitudes POST de creación responden 201; las consultas responden 200.

  • GET/healthVerifica que la llave es válida y se encuentra activa.
  • GET/companyDevuelve los datos del emisor.
  • POST/tokensCrea un token de acceso personal asociado a la llave de origen.
  • GET/tokensLista los tokens vigentes creados con esta llave.
  • DELETE/tokens/{id}Revoca de inmediato el token indicado.
  • GET/customersLista paginada de adquirientes (page, limit; offset heredado). Filtros nit o nombre (q). Totales en X-Total-Count.
  • POST/customersCrea un adquiriente. Si el NIT ya existe, responde 409.
  • GET/customers/{id}Consulta un adquiriente por su identificador.
  • PUT/customers/{id}Actualiza un adquiriente.
  • DELETE/customers/{id}Desactiva un adquiriente.
  • GET/customers/by-nit/{nit}Consulta un adquiriente por documento de identificación.
  • PUT/customers/by-nit/{nit}Crea o actualiza un adquiriente por NIT (sincronización).
  • POST/invoicesCrea una factura (01 venta, 02 exportación, 03 talonario/papel, 04 contingencia DIAN) o una nota (91/92) y la incorpora al flujo de transmisión ante la DIAN. Admite Idempotency-Key; un order_reference repetido en 01/02 responde 409.
  • GET/invoices/defaultsValores por defecto de emisión de la empresa: enabled_operation_types, defaults (entorno, forma y medio de pago, días de plazo, notas, operation_type), catálogos payment_forms / payment_means y el adquiriente consumidor_final.
  • POST/invoices/bulkEncola hasta 500 documentos para emisión asíncrona y devuelve un batch_id.
  • GET/invoices/bulk/{batch_id}Consulta el estado de un lote y el resultado de cada documento.
  • GET/invoicesLista paginada (page, limit ≤ 200) con filtros order_reference, invoice_number, customer_nit, dian_status, invoice_type (01/02/03/04/91/92/05/95), date_from, date_to, source. Totales en X-Total-Count.
  • GET/invoices/{id}Consulta una factura por su identificador. Incluye errors_explained (rechazo explicado) y dian_queue (estado de reintento) cuando aplican.
  • PUT/invoices/{id}Corrige un documento RECHAZADO o con ERROR sobre el mismo número: reconstruye el XML, recalcula el CUFE, firma y reenvía (finalize + send_to_dian). 409 invoice_not_correctable si ya fue transmitido.
  • POST/invoices/{id}/retry-nowTransmite ahora un documento pendiente por indisponibilidad de la DIAN (no espera el siguiente ciclo de 30 min).
  • POST/invoices/{id}/contingency-04Reemite una factura pendiente como tipo 04 (contingencia DIAN) con el mismo número, para entregarla al cliente sin validación previa.
  • GET/invoices/{id}/attached-documentDescarga el AttachedDocument (XML firmado + ApplicationResponse de la DIAN) del documento aceptado.
  • GET/invoices/{id}/application-responseDescarga solo el ApplicationResponse (acuse) de la DIAN del documento aceptado, application/xml.
  • GET/invoices/{id}/pdfDescarga la representación gráfica (PDF). Si el documento está ACEPTADO se sirve desde caché.
  • POST/invoices/{id}/pay-linkEmite (o devuelve) el enlace de pago «Ver y pagar» de una factura de venta o cotización abierta. 201 nuevo / 200 reutilizado; 409 paylink_not_eligible con reason.
  • GET/invoices/{id}/pay-linkEstado del enlace de pago más reciente (pending, paid, locked, revoked, disputed; expired se calcula al vencer expires_at). 404 no_pay_link.
  • DELETE/invoices/{id}/pay-linkRevoca el enlace vigente (el cliente pagó por otro medio, el pedido se canceló).
  • POST/invoices/{id}/paymentsRegistra un pago recibido fuera de la pasarela (efectivo, transferencia) sobre una factura emitida: actualiza saldo y Cartera, no toca el XML. reference única por factura.
  • GET/invoices/{id}/paymentsRecibos del documento y su saldo (total, retenciones, pagado, balance).
  • GET/payments/receiptsRecibos de toda la empresa (app, API y pasarela) por fechas y medio; company_id para otra empresa.
  • POST/payments/checkoutsCrea un cobro (pay / pay_and_invoice) y devuelve checkout_url. Ver Cobrar desde su web.
  • GET/payments/checkouts/{id}Estado del cobro y su factura.
  • GET/payments/capabilitiesEstado de WAVA/PayPal, límites y dominios de retorno.
  • GET/rangesLista las resoluciones de numeración vigentes.
  • POST/soporteCrea un documento soporte (05) o su nota de ajuste (95). Admite Idempotency-Key.
  • GET/soporteLista paginada de documentos soporte (05 y 95); mismos filtros que GET /invoices.
  • POST/payrollCrea una nómina electrónica (102) o de ajuste (103). Admite Idempotency-Key (huella: empleado + periodo + tipo).
  • GET/payrollLista paginada de nómina electrónica; filtros employee_doc, dian_status, period (YYYY-MM).
  • GET/payroll/{id}Consulta un documento de nómina electrónica. Incluye errors_explained y dian_queue cuando aplican.
  • PUT/payroll/{id}Corrige una nómina RECHAZADA o con ERROR sobre el mismo consecutivo (nuevo CUNE) y la reenvía.
  • POST/payroll/{id}/retry-nowTransmite ahora una nómina pendiente por indisponibilidad de la DIAN.
  • POST/payroll/{id}/signFirma un borrador de nómina electrónica.
  • POST/payroll/{id}/send-to-dianTransmite la nómina electrónica a la DIAN.
  • GET/payroll/{id}/statusConsulta el estado ante la DIAN por CUNE.
  • GET/payroll/{id}/pdfDescarga la representación gráfica de la nómina electrónica.
  • POST/eventsRegistra un evento RADIAN (eventos 030-034: acuse, reclamo, recibo del bien, aceptación expresa) sobre una factura de compra, identificado por CUFE. Valida el orden DIAN (409/422); 034 se rechaza con 422 event_not_allowed.
  • GET/events/{cufe}Consulta ante la DIAN el historial de eventos de una factura.
  • GET/usageConsulta el plan y el cupo de documentos restante.
  • GET/maintenanceVentana de mantenimiento vigente o próxima (o null). Nunca responde 503.
  • GET/webhooksLista los webhooks registrados.
  • POST/webhooksRegistra un webhook firmado; el secreto se exhibe una sola vez.
  • PATCH/webhooks/{id}Modifica el nombre, la URL, los eventos o el estado de activación.
  • POST/webhooks/{id}/rotateGenera un nuevo secreto de firma; se exhibe una sola vez.
  • DELETE/webhooks/{id}Elimina un webhook y su historial de entregas.
  • GET/catalogs/departmentsDepartamentos DANE (código y nombre).
  • GET/catalogs/departments/{code}/citiesMunicipios DANE de un departamento.
  • GET/catalogs/invoice-typesTipos de documento DIAN admitidos en la creación.
  • GET/catalogs/validate-nitValida un NIT y calcula su dígito de verificación.

Resoluciones de numeración

GET /ranges lista las resoluciones vigentes, con su prefijo, el rango autorizado y el próximo consecutivo. Permite consultar el prefijo que debe enviarse y la numeración restante, antes de emitir.

Solicitud

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

Respuesta 200

[
  {
    "id": "rng_01H8Y...",
    "company_id": "cmp_01H8Z...",
    "environment": "PRODUCTION",
    "prefix": "SETP",
    "resolution_number": "18760000001",
    "range_from": 990000000,
    "range_to": 995000000,
    "current_number": 990000123,
    "next_document": "SETP990000123",
    "valid_from": "2026-01-01",
    "valid_to": "2027-01-01",
    "is_active": true
  }
]

Tablas de códigos DIAN

Los campos codificados admiten únicamente los valores de estas tablas; cualquier otro produce respuesta 422, con la lista válida en el detalle. Son las mismas que valida el sistema, extraídas de la normativa DIAN.

Tratamiento del IVA por línea: exento se informa con tax_code 01 y tax_percent 0 (IVA a tarifa 0 %); excluido se informa con tax_excluded: true y la línea sale sin tributo (Anexo técnico 1.9, regla FAX01). El código de unidad se respeta tal como se envía: 94 vale tanto para servicios como para bienes sin unidad física (pines, recargas).

Tipos de documento de identidad (customer_type_doc / employee_doc_type)

CódigoDescripción
11Registro civil
12Tarjeta de identidad
13Cédula de ciudadanía
21Tarjeta de extranjería
22Cédula de extranjería
31NIT
41Pasaporte
42Documento de identificación extranjero
47PEP (Permiso Especial de Permanencia)
48PPT (Permiso por Protección Temporal)
50NIT de otro país
91NUIP

Forma de pago (payment_form)

CódigoDescripción
1Contado
2Crédito

Medios de pago (payment_means_code / payment_method_code)

CódigoDescripción
1Instrumento no definido
2Crédito ACH
3Débito ACH
10Efectivo
20Cheque
21Giro bancario (banker's draft)
30Transferencia crédito
31Transferencia débito
32Transferencia crédito (referenciada)
42Consignación bancaria
45Transferencia crédito bancaria
46Transferencia débito interbancaria
47Transferencia débito bancaria
48Tarjeta crédito
49Tarjeta débito
60Pagaré
61Nota promisoria firmada por acreedor
71Bonos
72Vales
ZZZOtro (mutuo acuerdo)

Tributos de línea (tax_code)

CódigoDescripciónTarifas (tax_percent)
01IVA0, 5 o 19 (exento = 0; excluido = tax_excluded: true)
03ICATarifa municipal (por mil, como porcentaje)
04INC4, 8 o 16
ZZOtros / no aplicaLibre
02 / 08 / 22 / 34 / 35IC, IC porcentual, INC bolsas plásticas, IBUA, ICUINo soportados: se liquidan por unidad; responden 422

Unidades de medida (unit_code)

CódigoDescripción
EACada uno (unidad de producto)
94Unidad (servicios y bienes sin unidad física, p. ej. pines o recargas)
NARNúmero de artículos
KGMKilogramo
LTRLitro
MTRMetro
HURHora
DAYDía
LUNMes
ANAAño
PKPaquete
BXCaja
ZZDefinida entre las partes

Responsabilidades fiscales (customer_fiscal_responsibility) — únicos códigos admitidos

CódigoDescripción
O-13Gran contribuyente
O-15Autorretenedor
O-23Agente de retención IVA
O-47Régimen simple de tributación
R-99-PNNo responsable / No aplica (predeterminado)

Catálogos de referencia

Las mismas tablas de códigos y de geografía que utiliza la aplicación para construir formularios y validar datos, disponibles si construye una interfaz sobre la API. Son datos de referencia: no leen ni escriben información de su instancia, de modo que pueden almacenarse en caché y consultarse sin restricción adicional.

GET /catalogs/departments devuelve los departamentos DANE (código y nombre) y GET /catalogs/departments/{code}/cities, sus municipios. GET /catalogs/invoice-types lista los tipos de documento DIAN admitidos en la creación. GET /catalogs/validate-nit?nit=900123456 valida un NIT y calcula su dígito de verificación, para que el formulario lo confirme antes de enviar.

Estas tablas cambian con muy poca frecuencia: consúltelas una vez al arrancar su integración y guárdelas en caché de forma local; no las solicite en cada operación. Lo mismo aplica a GET /health, que sirve para verificar la llave durante la configuración, no como sonda periódica.

Listar departamentos

curl -sS -X GET https://TU-EMPRESA.miplata.co/api/external/catalogs/departments \
  -H "X-API-Key: TU_LLAVE"
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/catalogs/departments", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
  },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/catalogs/departments",
    method="GET",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/catalogs/departments");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 200

[
  { "code": "05", "name": "Antioquia" },
  { "code": "08", "name": "Atlántico" },
  { "code": "11", "name": "Bogotá D.C." },
  { "code": "76", "name": "Valle del Cauca" }
]

Historial de cambios

Los cambios compatibles (campos nuevos en respuestas, filtros y endpoints adicionales) se publican sin cambiar la URL base. Un campo nuevo en una respuesta nunca se considera un cambio incompatible: ignore los que no conozca.

Versiones

VersiónFechaCambios
1.2023 oct 2026Documento soporte: campo opcional ds_period (operation / weekly) en POST /soporte, con cac:InvoicePeriod por línea (Res. 000167/2021, DSFC02c); emisión del 05 desde Compras en la aplicación. Sin cambios incompatibles.
1.1923 oct 2026Numeración propia de notas (Res. 000042/2020 art. 7; Res. 000167/2021 art. 9): las notas crédito (91), débito (92) y de ajuste al documento soporte (95) ya no consumen el rango autorizado de factura ni de documento soporte. Consecutivo propio por tipo, empresa y ambiente: NC<n>, ND<n>, NAS<n>, desde 1 (o tras el mayor ya emitido). El XML de las notas omite sts:InvoiceControl; en InvoiceOut resolution_number, range_from y range_to son null para las notas. Sin cambios en la solicitud.
1.1823 oct 2026Asistentes IA: sección Conectar ChatGPT y Claude (conector MCP en mcp.miplata.co, presets assistant_read / assistant_write, código MPC, OAuth 2.1; requiere activación por miPlata) y sección Asistente de tienda (Lucas Tienda: canal API POST /v1/{bot}/answer y /feedback con X-Bot-Key en bot.miplata.co, widget, límites y códigos).
1.1722 oct 2026Auditoría de la API. Idempotency-Key en POST /soporte y POST /payroll (registro transaccional, sin duplicados tras un fallo). Cupo verificado antes de numerar (402 quota_blocked sin consumir consecutivo); Sandbox y borradores nunca reciben 402 en el borde. Licencia vencida (402 licence_expired + X-MiPlata-Licence) y cuenta suspendida (403 account_suspended) también en cobros. Regenerar la llave revoca sus PAT (revoked_child_tokens); los PAT siguen la lista de IP vigente de su llave. Validación fiscal: IVA 0/5/19, INC 4/8/16, tributos por unidad 02/08/22/34/35 → 422, ZZ = Otros / no aplica, responsabilidades fuera de O-13/O-15/O-23/O-47/R-99-PN → 422, municipio DIVIPOLA del departamento, esquema del adquiriente 01 con régimen 48, redondeo half-up con PayableRoundingAmount. RADIAN: orden 030 → 032 → 031/033 (409), 034 → 422 event_not_allowed, receiver_person. Webhooks: patrones familia.*, 6 intentos (30 s, 2 min, 10 min, 1 h, 6 h), X-MiPlata-Timestamp + X-MiPlata-Signature-V2, X-MiPlata-Attempt; eventos de nómina corregidos. GET /invoices/defaults. Paginación con X-Total-* en GET /customers y GET /payments/receipts. Tipos 03/04 en invoice_type y filtros; dian_queue CANCELLED; motivos de paylink_not_eligible; enlaces de pago: pagar no pide dígitos, QR automático y «Sin enlace de pago» por cliente.
1.1616 oct 2026Tipos de operación DIAN (Anexo 1.9): operation_type 09 AIU (aiu, aiu_component), 11 mandatos (line_scheme, mandante), 12 transporte de carga (line_scheme, remesas), tipo 03 talonario (talonario), notas sin referencia 22 / 32 (note_period_start / note_period_end); operation_type en InvoiceOut; nota débito con CustomizationID 30 y exportación con 10.
1.1512 oct 2026Nómina: medio de pago predeterminado 47; muestras con el SMLMV 2026; campos de solicitud documentados (employee_id, employee_pensioned, employee_health_regime, employee_worker_type, employee_arl_class, withholding_*, overtime, bonus_amount, transport_allowance, pay_by_day, settlement_*, contract_*); liquidación automática con FSP, retención procedimiento 1, art. 114-1, topes del IBC, Ley 1393 y tipos de cotizante 19/21/23/51.
1.1410 oct 2026Pagos recibidos fuera de la pasarela: POST /invoices/{id}/payments (efectivo, transferencia; reference única por factura, 409 overpayment / already_paid / not_payable), GET /invoices/{id}/payments, GET /payments/receipts para el resumen contable, objeto payment en POST /invoices (la factura nace pagada) y balance en todo InvoiceOut. Los pagos de enlaces y cobros no se registran dos veces.
1.139 oct 2026Retenciones: código 06 se informa como ReteRenta con base y tarifa reales (ReteIVA sobre el IVA, ReteICA por mil); 422 withholding_invalid con errors[] por campo; notas sin retenciones; saldo de cartera y enlace de pago = total − retenciones.
1.129 oct 2026Cobros con llave API (POST /payments/checkouts pay | pay_and_invoice, expires_in_minutes, eventos checkout.*); return_url en enlaces de pago y cobros (Dominios de retorno); GET /payments/capabilities con bloque wava; quote_id en invoice.paid; 402 quota_blocked con code y texto en español; guía Cobrar desde su web; precios y cupo en una sección.
1.111 oct 2026Bloque source en InvoiceOut (origen del documento) y filtro source en listados; Cobros (/payments/capabilities, /buyer/validate, /checkouts, cancel, credit-note, documentos); Conexiones de tienda (protocolo firmado v1, código de conexión, webhooks firmados hacia la tienda); Datos del comprador (DIVIPOLA, DV, políticas missing_data); plugins oficiales WooCommerce y Tiendanube; PayPal en USD/EUR/GBP/CAD/CHF/MXN/JPY sin conversión.
1.1021 sep 2026Emisión masiva (/invoices/bulk), eventos de lote invoice_batch.*, aviso maintenance.scheduled.
1.9ago 2026Nómina electrónica por API, notas de ajuste 103, eventos payroll.*.

Puesta en marcha

Verificación de la llave

Inicie con /health: confirma que la llave autentica y devuelve su nombre. La respuesta 401 indica que la llave falta o no existe; la respuesta 403 indica que existe, pero está vencida, no tiene direcciones IP configuradas o la solicitud proviene de una dirección IP no permitida.

Solicitud

curl -sS -X GET https://TU-EMPRESA.miplata.co/api/external/health \
  -H "X-API-Key: TU_LLAVE"
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/health", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
  },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/health",
    method="GET",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/health");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 200

{
  "status": "ok",
  "api_key_name": "erp-produccion"
}

Datos del emisor

GET /company devuelve los datos del emisor: NIT, régimen, municipio y el campo mode (TEST o PRODUCTION), que indica si la instancia se encuentra en Sandbox o en producción. Permite verificar contra qué empresa —y en qué entorno— se emitirá, antes de transmitir el primer documento.

Solicitud

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

Respuesta 200

{
  "id": "cmp_01H8Z...",
  "name": "Mi Empresa SAS",
  "trade_name": "Mi Empresa",
  "nit": "901234567",
  "nit_dv": "8",
  "type_doc": "31",
  "person_type": "1",
  "tax_regime": "48",
  "tax_level": "O-13",
  "city_code": "11001",
  "department_code": "11",
  "country": "CO",
  "mode": "PRODUCTION",
  "is_active": true,
  "created_at": "2026-01-15T14:03:22Z"
}

Adquirientes

Su sistema ya dispone de un maestro de clientes; por ello la API expone el mismo directorio que utiliza la aplicación. Una vez sincronizado, la emisión se reduce a customer_nit e items. Los adquirientes creados por API se reflejan en la aplicación, y los existentes en ella pueden consultarse y actualizarse desde aquí.

Para la sincronización utilice PUT /customers/by-nit/{nit}: crea el adquiriente si no existe y lo actualiza si ya está registrado, de modo que el maestro puede reenviarse las veces que se requiera, sin duplicar registros. POST /customers es la creación estricta: si el NIT ya existe, responde 409 con el identificador del adquiriente que lo posee.

GET /customers?nit=900123456 permite comprobar si un adquiriente ya está registrado, antes de emitir. El campo custom_fields admite el identificador que ese cliente tiene en su sistema de origen, para mantener ambas bases conciliadas. DELETE desactiva el adquiriente; los documentos ya emitidos conservan su propia copia de los datos.

Cuerpo de la solicitud (cliente)

CampoTipoReq.Descripción
namestringSíNombre o razón social (máximo 255 caracteres).
nitstringDocumento de identificación, sin dígito de verificación. En PUT /customers/by-nit/{nit} se toma de la ruta.
type_docstringTipo de documento de identidad DIAN. Valor predeterminado: 31 (NIT).
person_typestring1 = Persona jurídica, 2 = Persona natural. Valor predeterminado: 2.
emailstringCorreo electrónico al que se envía la representación gráfica.
phonestringTeléfono de contacto.
addressstringDirección (máximo 500 caracteres).
citystringNombre del municipio.
city_codestringCódigo DANE del municipio, de 5 dígitos (11001 = Bogotá D.C.).
department_codestringCódigo DANE del departamento, de 2 dígitos.
countrystringCódigo ISO 3166-1 alpha-2. Valor predeterminado: CO.
tax_regimestring48 = Responsable de IVA, 49 = No responsable de IVA. Valor predeterminado: 49.
tax_levelstringCódigo de responsabilidades o nivel tributario (O-13, O-49, entre otros). Valor predeterminado: O-49.
fiscal_responsibilitystringUno o varios códigos separados por ';'. Valor predeterminado: R-99-PN.
is_consumidor_finalbooltrue para el adquiriente genérico Consumidor Final.
is_foreignbooltrue si el adquiriente es del exterior; sus facturas se emiten como exportación (02).
custom_fieldsobjectPares clave-valor propios (por ejemplo, el identificador del cliente en su sistema de origen).

Sincronizar un cliente por NIT (crea o actualiza)

curl -sS -X PUT https://TU-EMPRESA.miplata.co/api/external/customers/by-nit/900123456 \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Acme SAS",
  "type_doc": "31",
  "email": "pagos@acme.co",
  "address": "Calle 100 # 20-30",
  "city_code": "11001",
  "department_code": "11",
  "person_type": "1",
  "tax_regime": "48",
  "tax_level": "O-13",
  "fiscal_responsibility": "O-13;O-15"
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/customers/by-nit/900123456", {
  method: "PUT",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "name": "Acme SAS",
  "type_doc": "31",
  "email": "pagos@acme.co",
  "address": "Calle 100 # 20-30",
  "city_code": "11001",
  "department_code": "11",
  "person_type": "1",
  "tax_regime": "48",
  "tax_level": "O-13",
  "fiscal_responsibility": "O-13;O-15"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "name": "Acme SAS",
  "type_doc": "31",
  "email": "pagos@acme.co",
  "address": "Calle 100 # 20-30",
  "city_code": "11001",
  "department_code": "11",
  "person_type": "1",
  "tax_regime": "48",
  "tax_level": "O-13",
  "fiscal_responsibility": "O-13;O-15"
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/customers/by-nit/900123456",
    data=payload,
    method="PUT",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/customers/by-nit/900123456");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "name" => "Acme SAS",
    "type_doc" => "31",
    "email" => "pagos@acme.co",
    "address" => "Calle 100 # 20-30",
    "city_code" => "11001",
    "department_code" => "11",
    "person_type" => "1",
    "tax_regime" => "48",
    "tax_level" => "O-13",
    "fiscal_responsibility" => "O-13;O-15"
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 200

{
  "id": "cli_01H9D...",
  "company_id": "cmp_01H8Z...",
  "name": "Acme SAS",
  "nit": "900123456",
  "type_doc": "31",
  "person_type": "1",
  "email": "pagos@acme.co",
  "address": "Calle 100 # 20-30",
  "city_code": "11001",
  "department_code": "11",
  "country": "CO",
  "country_name": "Colombia",
  "tax_regime": "48",
  "tax_level": "O-13",
  "fiscal_responsibility": "O-13;O-15",
  "is_consumidor_final": false,
  "is_foreign": false,
  "is_active": true,
  "created_at": "2026-09-08T12:00:00Z"
}

Facturación

Crear una factura

POST /invoices crea una factura de venta (01) o de exportación (02) y, salvo que se indique lo contrario, la firma y la transmite a la DIAN en la misma solicitud.

Únicamente se requieren dos elementos: el adquiriente y las líneas. Si el NIT ya existe como cliente (creado con POST /customers o registrado en la aplicación), es suficiente enviar customer_nit e items: el nombre, el municipio, el régimen y los demás datos se toman de su ficha. Si el NIT no está registrado, incluya también customer_name y los datos que desee conservar.

Los demás campos tienen valores predeterminados conformes a la práctica comercial: contado, efectivo, IVA del 19 % y adquiriente no responsable de IVA. La respuesta es el documento creado, con su CUFE, su número y su estado de transmisión.

Tres tratamientos de IVA por línea: gravado (tax_code 01 y tax_percent 19, 5…), exento (tax_code 01 y tax_percent 0: la línea lleva IVA a tarifa 0 %) y excluido (tax_excluded: true: la línea no lleva bloque de impuesto, conforme a la regla FAX01 del Anexo técnico 1.9, y su valor queda fuera de TaxExclusiveAmount). Pines y recargas de telefonía, por ejemplo, son excluidos. Una factura puede mezclar los tres.

Cuerpo de la solicitud (InvoiceCreateRequest)

CampoTipoReq.Descripción
invoice_typestring01 = Factura de venta (predeterminado), 02 = Factura de exportación, 03 = Factura de talonario o papel (contingencia del facturador, con talonario), 04 = Factura de contingencia DIAN, 91 = Nota crédito, 92 = Nota débito; 05 = Documento soporte y 95 = Nota de ajuste solo por POST /soporte. Consulte la tabla de códigos.
customer_nitstringSíDocumento de identificación del adquiriente, sin dígito de verificación (el DV se calcula de forma automática). Se valida según customer_type_doc: el NIT (31) exige dígitos válidos; la cédula de ciudadanía (13) admite entre 3 y 15 dígitos, entre otros. Puede omitirse únicamente cuando se envía is_consumidor_final=true. Si el NIT ya está registrado como cliente, es suficiente este campo: los demás datos se toman de su ficha.
customer_namestringNombre o razón social (máximo 255 caracteres). Obligatorio en la primera emisión a ese NIT; si el adquiriente ya es cliente, o si is_consumidor_final=true, se completa de forma automática.
customer_type_docstringTipo de documento de identidad DIAN. Valor predeterminado: 31 (NIT). Consulte la tabla de tipos de documento.
customer_emailstringCorreo electrónico del adquiriente. La representación gráfica se envía a esta dirección.
customer_phonestringCelular del adquiriente. Con is_consumidor_final=true es obligatorio si va a cobrar con enlace de pago: sus últimos 4 dígitos desbloquean la página (sin él, 409 no_unlock_digits).
customer_addressstringDirección del adquiriente (máximo 500 caracteres).
customer_city_codestringCódigo DANE del municipio, de 5 dígitos (por ejemplo, 11001 = Bogotá D.C.). Debe existir en DIVIPOLA y sus dos primeros dígitos deben coincidir con customer_department_code; de lo contrario 422 (GET /catalogs/departments/{code}/cities).
customer_department_codestringCódigo DANE del departamento, de 2 dígitos.
customer_countrystringCódigo ISO 3166-1 alpha-2. Valor predeterminado: CO.
customer_person_typestring1 = Persona jurídica, 2 = Persona natural. Valor predeterminado: 2.
customer_tax_regimestring48 = Responsable de IVA, 49 = No responsable de IVA. Valor predeterminado: 49. Con 48 el esquema tributario del adquiriente en el XML es 01 IVA; con 49, ZZ.
customer_tax_levelstringCódigo de responsabilidades o nivel tributario (por ejemplo, O-13, O-49). Valor predeterminado: O-49.
customer_fiscal_responsibilitystringUno o varios códigos separados por ';' (por ejemplo, O-13;O-15). Valor predeterminado: R-99-PN. Solo se admiten los que la DIAN acepta en el XML: O-13, O-15, O-23, O-47 y R-99-PN; cualquier otro código responde 422.
is_consumidor_finalboolSi es true, se diligencian de forma automática los datos del Consumidor Final (NIT 222222222222).
payment_formstring1 = Contado (predeterminado), 2 = Crédito.
payment_means_codestringMedio de pago DIAN. Valor predeterminado: 10 (efectivo). Consulte la tabla de medios de pago.
payment_daysintPlazo en días hasta el vencimiento cuando payment_form=2 (0–365).
payment_due_datestringFecha de vencimiento en formato AAAA-MM-DD (alternativa a payment_days).
itemsarraySíDebe incluir al menos una línea. Consulte los campos de ítem más adelante.
currencystringMoneda ISO 4217 (COP, valor predeterminado). Admitidas: COP, USD, EUR, GBP, JPY, CHF, CAD, MXN, BRL, ARS, PEN y CLP. Si es distinta de COP, la instancia aplica la tasa vigente del día (USD→COP corresponde a la TRM oficial de datos.gov.co; las demás se calculan con las tasas del BCE frente a la TRM) y convierte el total a pesos colombianos para la DIAN. El documento conserva los importes en la moneda original y registra la tasa aplicada, la fecha de la tasa y el total en COP. No envíe la tasa: utilice exchange_rate únicamente si requiere forzar un valor distinto al vigente. En las notas (91, 92 y 95) en la misma moneda que el documento original, si no envía exchange_rate se aplica la tasa del original (no la del día), de modo que una anulación cancela exactamente los valores en COP aceptados por la DIAN.
notesstringNota del documento (cbc:Note de la DIAN; máximo 5.000 caracteres).
correction_invoice_idstringIdentificador de la factura o del documento original. Obligatorio para los tipos 91, 92 y 95.
ds_periodobjectSolo tipo 05: { mode, start }. mode operation (predeterminado, código 1: una operación, start = fecha de emisión) o weekly (código 2: acumulado semanal, start hasta 6 días antes de la emisión; si no, 400). Va en cac:InvoicePeriod de cada línea (Res. 000167/2021).
correction_conceptstringCódigo del concepto de corrección. Consulte la tabla de conceptos de nota.
correction_reasonstringMotivo de la nota (máximo 500 caracteres).
withholding_tax_amountfloatReteRenta (DIAN 06) que practicará el adquiriente, sobre el valor antes de IVA. Informativa: no cambia el total. Ver Retenciones.
withholding_iva_amountfloatReteIVA (DIAN 05), sobre el IVA de la factura. Ver Retenciones.
withholding_ica_amountfloatReteICA (DIAN 07), por mil sobre el valor antes de IVA. Ver Retenciones.
prepaid_amountfloatAnticipo ya recibido, que se resta del total a pagar (va al XML como anticipo).
paymentobjectPago ya recibido fuera de la pasarela: { payment_method, reference, amount?, payment_date? }. La factura nace pagada; no cambia el XML. Ver Pagos recibidos fuera de la pasarela.
order_referencestringNúmero de orden de compra del adquiriente (máximo 100 caracteres).
delivery_countrystringPaís de entrega (ISO alpha-2) en facturas de exportación (02).
delivery_terms_codestringIncoterm aplicable a la exportación (FOB, CIF, EXW, entre otros).
project_idstringProyecto o marca bajo el cual se emite el documento (utiliza su logotipo).
languagestring'es' (predeterminado) o 'en' para la representación gráfica.
save_onlyboolSi es true, el documento se guarda como borrador (GENERATED), sin firma ni transmisión.
send_to_dianboolSi es true (valor predeterminado), el documento se transmite a la DIAN después de firmarse.
is_quoteboolSi es true, se crea una cotización (no consume consecutivo DIAN). Con POST /invoices/{id}/pay-link se cobra; al pagarse se convierte en factura.
quote_expires_daysintDías de vigencia de la cotización (1–365; valor predeterminado: 30).
custom_fieldsobjectPares clave-valor de uso libre que se almacenan con el documento.

items[] — cada línea

CampoTipoReq.Descripción
descriptionstringSíDescripción de la línea (cac:Item/cbc:Description).
quantityfloatSíCantidad, mayor que 0.
unit_pricefloatSíPrecio unitario efectivamente cobrado (≥ 0).
tax_percentfloatPorcentaje del impuesto de la línea. Valor predeterminado: 19. Debe ser una tarifa legal: IVA (01) 0, 5 o 19; INC (04) 4, 8 o 16; otra tarifa responde 422 antes de numerar.
tax_codestringTipo de tributo DIAN: 01 IVA (predeterminado), 03 ICA, 04 INC o ZZ (otros / no aplica). Los tributos por unidad (02, 08, 22, 34, 35) aún no se admiten y responden 422. Consulte la tabla de tributos.
namestringNombre corto del bien o servicio (cac:Item/cbc:Name).
regular_pricefloatPrecio de lista antes de descuento (PricingReference).
unit_codestringCódigo de unidad de medida DIAN. Valor predeterminado: EA. Consulte la tabla de unidades.
is_servicebooltrue para servicios; si unit_code permanece en EA, se ajusta a 94.
tax_excludedbooltrue para bienes o servicios excluidos de IVA (ET art. 424 y 476; recargas y pines de telefonía, art. 420). La línea se emite sin cac:TaxTotal (Anexo técnico 1.9, regla FAX01) y no entra en TaxExclusiveAmount. No es lo mismo que exento: un exento se envía con tax_code 01 y tax_percent 0.

Solicitud mínima — NIT conocido + líneas

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "customer_nit": "900123456",
  "items": [
    { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000 }
  ]
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/invoices", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "customer_nit": "900123456",
  "items": [
    { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000 }
  ]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "customer_nit": "900123456",
  "items": [
    { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000 }
  ]
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/invoices",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/invoices");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "customer_nit" => "900123456",
    "items" => [
        [
            "description" => "Consultoría de software",
            "quantity" => 10,
            "unit_price" => 150000
        ]
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201

{
  "id": "inv_01H9A...",
  "company_id": "cmp_01H8Z...",
  "environment": "PRODUCTION",
  "invoice_number": "SETP990000123",
  "invoice_type": "01",
  "prefix": "SETP",
  "cufe": "a1b2c3d4e5f6...9f",
  "qr_code": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=a1b2...",
  "dian_status": "SIGNED",
  "customer_name": "Acme SAS",
  "customer_nit": "900123456",
  "subtotal_amount": "1520000.00",
  "tax_total_amount": "285000.00",
  "total_amount": "1805000.00",
  "currency": "COP",
  "resolution_number": "18760000001",
  "line_items": [
    {
      "line_number": 1,
      "description": "Consultoría de software",
      "quantity": "10.00",
      "unit_price": "150000.00",
      "tax_percent": "19.00",
      "tax_code": "01",
      "line_extension": "1500000.00",
      "tax_amount": "285000.00",
      "unit_code": "HUR",
      "is_service": true,
      "tax_excluded": false
    },
    {
      "line_number": 2,
      "description": "Pin recarga celular $20.000",
      "quantity": "1.00",
      "unit_price": "20000.00",
      "tax_percent": "0.00",
      "tax_code": "01",
      "line_extension": "20000.00",
      "tax_amount": "0.00",
      "unit_code": "94",
      "is_service": false,
      "tax_excluded": true
    }
  ],
  "issue_date": "2026-09-08T12:34:56Z",
  "created_at": "2026-09-08T12:34:56Z"
}

Factura con todos los datos del cliente

En la primera emisión a un adquiriente, o cuando requiera definir cada campo, envíe el bloque customer_* completo. La instancia lo utiliza tal como se envía y lo registra como cliente, de modo que la siguiente factura a ese NIT puede enviarse en su forma mínima.

Solicitud — adquiriente completo

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": "01",
  "customer_name": "Acme SAS",
  "customer_type_doc": "31",
  "customer_nit": "900123456",
  "customer_email": "pagos@acme.co",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "customer_person_type": "1",
  "customer_tax_regime": "48",
  "customer_tax_level": "O-13",
  "customer_fiscal_responsibility": "O-13;O-15",
  "payment_form": "1",
  "payment_means_code": "10",
  "notes": "Gracias por su compra",
  "items": [
    {
      "description": "Consultoría de software",
      "name": "Consultoría",
      "quantity": 10,
      "unit_price": 150000,
      "tax_percent": 19,
      "tax_code": "01",
      "unit_code": "HUR",
      "is_service": true
    },
    {
      "description": "Pin recarga celular $20.000",
      "quantity": 1,
      "unit_price": 20000,
      "unit_code": "94",
      "tax_excluded": true
    }
  ]
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/invoices", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "invoice_type": "01",
  "customer_name": "Acme SAS",
  "customer_type_doc": "31",
  "customer_nit": "900123456",
  "customer_email": "pagos@acme.co",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "customer_person_type": "1",
  "customer_tax_regime": "48",
  "customer_tax_level": "O-13",
  "customer_fiscal_responsibility": "O-13;O-15",
  "payment_form": "1",
  "payment_means_code": "10",
  "notes": "Gracias por su compra",
  "items": [
    {
      "description": "Consultoría de software",
      "name": "Consultoría",
      "quantity": 10,
      "unit_price": 150000,
      "tax_percent": 19,
      "tax_code": "01",
      "unit_code": "HUR",
      "is_service": true
    },
    {
      "description": "Pin recarga celular $20.000",
      "quantity": 1,
      "unit_price": 20000,
      "unit_code": "94",
      "tax_excluded": true
    }
  ]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "invoice_type": "01",
  "customer_name": "Acme SAS",
  "customer_type_doc": "31",
  "customer_nit": "900123456",
  "customer_email": "pagos@acme.co",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "customer_person_type": "1",
  "customer_tax_regime": "48",
  "customer_tax_level": "O-13",
  "customer_fiscal_responsibility": "O-13;O-15",
  "payment_form": "1",
  "payment_means_code": "10",
  "notes": "Gracias por su compra",
  "items": [
    {
      "description": "Consultoría de software",
      "name": "Consultoría",
      "quantity": 10,
      "unit_price": 150000,
      "tax_percent": 19,
      "tax_code": "01",
      "unit_code": "HUR",
      "is_service": true
    },
    {
      "description": "Pin recarga celular $20.000",
      "quantity": 1,
      "unit_price": 20000,
      "unit_code": "94",
      "tax_excluded": true
    }
  ]
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/invoices",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/invoices");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "invoice_type" => "01",
    "customer_name" => "Acme SAS",
    "customer_type_doc" => "31",
    "customer_nit" => "900123456",
    "customer_email" => "pagos@acme.co",
    "customer_city_code" => "11001",
    "customer_department_code" => "11",
    "customer_person_type" => "1",
    "customer_tax_regime" => "48",
    "customer_tax_level" => "O-13",
    "customer_fiscal_responsibility" => "O-13;O-15",
    "payment_form" => "1",
    "payment_means_code" => "10",
    "notes" => "Gracias por su compra",
    "items" => [
        [
            "description" => "Consultoría de software",
            "name" => "Consultoría",
            "quantity" => 10,
            "unit_price" => 150000,
            "tax_percent" => 19,
            "tax_code" => "01",
            "unit_code" => "HUR",
            "is_service" => true
        ],
        [
            "description" => "Pin recarga celular $20.000",
            "quantity" => 1,
            "unit_price" => 20000,
            "unit_code" => "94",
            "tax_excluded" => true
        ]
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Retenciones (ReteRenta, ReteIVA, ReteICA)

Las retenciones las practica el adquiriente cuando paga, si es agente de retención; en la factura electrónica son informativas (Anexo técnico 1.9, grupo WithholdingTaxTotal): la DIAN no las resta del total (PayableAmount). Envíelas solo cuando su cliente le indique cuánto le retendrá. La instancia calcula la base y la tarifa que exige la DIAN a partir de los ítems; usted envía solo el valor.

El saldo en cartera y el enlace de pago usan total − anticipo − retenciones: es lo que el cliente le transfiere. En POST /soporte (05) son las que usted, como comprador, practica al vendedor. En un cobro (POST /payments/checkouts) solo se aceptan con mode invoice; un cobro en línea cobra el total.

  • Notas 91, 92 y 95: las retenciones se ignoran (UBL 2.1 no tiene ese grupo en notas).
  • Cada error indica field (p. ej. withholding_iva_amount), code y message en español.

Qué se envía y cuándo aplica

CampoDIANBaseTarifa (2026)No aplica
withholding_tax_amount06 ReteRentaValor antes de IVAServicios 4 % (6 % no declarante) desde 2 UVT; compras 2,5 % desde 10 UVT; honorarios 10–11 %Emisor en Régimen SIMPLE (O-47); exportación; consumidor final
withholding_iva_amount05 ReteIVAIVA de la factura15 % (100 % casos especiales); solo la practican O-13 y O-23Factura sin IVA; exportación; consumidor final
withholding_ica_amount07 ReteICAValor antes de IVAPor mil, según el municipio (p. ej. 9,66 ‰)Emisor SIMPLE; documento soporte; exportación; consumidor final

422 withholding_invalid — errors[].code

codeCausa
withholding_exceeds_baseEl valor supera su base.
withholding_no_baseNo hay base (p. ej. ReteIVA en una factura sin IVA).
withholding_rate_mismatchNinguna tarifa sobre la base produce ese valor (regla FAT07).
withholding_simple_regimeEmisor en el Régimen SIMPLE: sin ReteRenta ni ReteICA (ET art. 911).
withholding_not_on_export / withholding_not_consumidor_finalExportación o consumidor final: no llevan retenciones.
withholding_not_in_soporteReteICA en un documento soporte.

Servicio de $1.000.000 + IVA: ReteRenta 4 %, ReteIVA 15 %, ReteICA 9,66 ‰ → total 1.190.000, neto 1.111.840

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{ "customer_nit": "900123456", "items": [{ "description": "Consultoría", "quantity": 1, "unit_price": 1000000, "tax_percent": 19, "is_service": true }],
        "withholding_tax_amount": 40000, "withholding_iva_amount": 28500, "withholding_ica_amount": 9660 }'

Consultas paginadas y filtros

GET /invoices, GET /soporte y GET /payroll devuelven una página, no la tabla completa: del más reciente al más antiguo, 50 registros por defecto (limit, máximo 200) y page desde 1. El cuerpo sigue siendo un arreglo JSON; el total viaja en los encabezados X-Total-Count, X-Total-Pages, X-Page y X-Per-Page. Recorra las páginas hasta que X-Page alcance X-Total-Pages (o hasta recibir un arreglo vacío).

Filtros de GET /invoices y GET /soporte (se combinan con Y lógico): order_reference (igualdad exacta), invoice_number (prefijo + consecutivo, p. ej. FE1501), customer_nit (sin dígito de verificación), dian_status (DRAFT, PENDING, SENT, ACCEPTED, REJECTED), invoice_type (01, 02, 03, 04, 91, 92, 05, 95; GET /soporte solo 05 y 95), date_from y date_to (YYYY-MM-DD sobre issue_date, ambos inclusive), y source (origen del documento: manual, pos, shop, api, connection, o conn:<id> para una conexión de tienda concreta; véase Origen). GET /payroll admite employee_doc, dian_status y period (YYYY-MM).

Para conciliar un pedido utilice GET /invoices?order_reference=ORD-1001: responde el documento (o un arreglo vacío) en una sola consulta, sin descargar el historial. Para sincronizar, consulte por fechas o suscríbase a los webhooks; evite recorrer todas las páginas en cada ciclo.

  • page — entero ≥ 1 (predeterminado 1).
  • limit — entero entre 1 y 200 (predeterminado 50).
  • Encabezados de respuesta: X-Total-Count, X-Total-Pages, X-Page, X-Per-Page.
  • Una página vacía ([]) con 200 indica que no hay más resultados.

Buscar la factura de un pedido

curl -sS "https://TU-EMPRESA.miplata.co/api/external/invoices?order_reference=ORD-1001&limit=1" \
  -H "X-API-Key: TU_LLAVE" -D -

Respuesta 200

HTTP/2 200
x-total-count: 1
x-total-pages: 1
x-page: 1
x-per-page: 1

[ { "id": "…", "invoice_number": "FE1501", "order_reference": "ORD-1001", "dian_status": "ACCEPTED", "cufe": "…" } ]

Reintentos sin duplicados

Un POST /invoices que se reintenta tras un timeout o un error de red no debe emitir dos facturas. Hay dos mecanismos, complementarios y opcionales, además del client_reference de POST /invoices/bulk.

Encabezado Idempotency-Key: un identificador suyo, único por solicitud lógica (8 a 128 caracteres [A-Za-z0-9_.:-], por ejemplo un UUID). Si repite la misma solicitud con la misma clave durante 24 horas, la respuesta es 200 con el documento original y el encabezado Idempotent-Replayed: true; no se emite ni se transmite otro documento. Reutilizar la clave con un cuerpo distinto responde 422. La comparación se hace sobre el contenido JSON tal como usted lo envía, antes de que el servidor complete datos (cliente, valores por defecto): el orden de los campos y los espacios no cuentan; cambiar un valor, sí.

order_reference único en ventas: una factura de venta o de exportación (01/02) cuyo order_reference ya existe en la instancia —en el mismo entorno— se rechaza con 409; detail incluye code order_reference_exists y el invoice_id, invoice_number, cufe y dian_status del documento existente, para que su sistema lo adopte en lugar de reintentar. Las notas (91/92) pueden repetir el order_reference de la factura que corrigen.

Dos reintentos simultáneos con la misma clave o el mismo order_reference se procesan uno después del otro: el primero emite, el segundo recibe la repetición (200) o el 409.

Idempotency-Key funciona igual en POST /soporte (05/95) y POST /payroll (102/103). En nómina, dos creaciones simultáneas para el mismo empleado, periodo y tipo se procesan una después de la otra, de modo que dos reintentos en vuelo nunca toman dos consecutivos.

El registro de la clave se guarda en la misma transacción que el documento: si la respuesta se pierde después de crearlo, el reintento con la misma clave encuentra el documento y no emite otro.

Emisión segura ante reintentos

curl -sS https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" \
  -H "Idempotency-Key: 5b0c7e2a-3f1d-4c55-9b7e-2d1a0f6c9e11" \
  -H "Content-Type: application/json" \
  -d '{ "customer_nit": "900123456", "order_reference": "ORD-1001", "items": [ { "description": "Servicio", "quantity": 1, "unit_price": 100000 } ] }'

Respuesta 409 — el pedido ya fue facturado

{
  "detail": {
    "code": "order_reference_exists",
    "message": "A 01 document already carries order_reference 'ORD-1001'",
    "invoice_id": "3f2b…",
    "invoice_number": "FE1501",
    "cufe": "…",
    "dian_status": "ACCEPTED"
  }
}

Casos por tipo de cliente

Estos cuatro casos cubren las combinaciones de uso: nacional o extranjero, y consumidor final o identificado. Todos se someten a las mismas validaciones de la aplicación y pueden utilizarse como referencia.

Reglas que aplica la instancia de forma automática: si is_consumidor_final=true, el NIT se asigna como 222222222222 y no es necesario enviar más datos del adquiriente. Si customer_country es distinto de CO (o el tipo de documento es extranjero), la factura de venta 01 se convierte en factura de exportación 02, el IVA de cada línea queda en 0 y —si la moneda no es COP— el total se convierte a pesos colombianos con la tasa vigente. No reproduzca la dirección de su empresa en un adquiriente del exterior: omítala y la instancia utilizará el país.

Campos mínimos y opcionales por caso

CasoMínimo obligatorioOpcionales frecuentes
1 · Nacional · consumidor finalis_consumidor_final=true, itemscurrency, notes, payment_form, project_id
2 · Nacional · identificadocustomer_nit, customer_name (1ª vez), itemscustomer_type_doc, customer_person_type, customer_tax_regime, customer_tax_level, customer_fiscal_responsibility, customer_email, customer_city_code, customer_department_code
3 · Extranjero · consumidor finalis_consumidor_final=true, customer_country, itemscustomer_country_name, currency, delivery_terms_code
4 · Extranjero · identificadocustomer_name, customer_country, itemscustomer_nit, customer_type_doc (50/42), customer_person_type, customer_address, customer_email, currency, delivery_terms_code

Caso 1 — nacional, consumidor final

# 1) NACIONAL · CONSUMIDOR FINAL
# Campos mínimos: el indicador y las líneas. La instancia asigna el NIT
# genérico 222222222222 y los datos del Consumidor Final nacional.
curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{
  "is_consumidor_final": true,
  "items": [
    { "description": "Venta de mostrador", "quantity": 1, "unit_price": 50000 }
  ]
}'

Caso 2 — nacional identificado

Persona jurídica con NIT (customer_type_doc 31) o persona natural con cédula de ciudadanía (13). En la primera emisión envíe customer_nit y customer_name; en las siguientes es suficiente customer_nit e items, porque los demás datos (tipo de persona, régimen, nivel y responsabilidades fiscales) se toman de la ficha registrada. El NIT se valida según el tipo de documento y el dígito de verificación se calcula de forma automática.

Caso 2 — nacional identificado

# 2) NACIONAL · IDENTIFICADO (persona jurídica con NIT o persona natural con cédula)
# En la primera emisión: customer_nit, customer_name e items. En emisiones
# posteriores es suficiente customer_nit e items. Los demás campos son
# opcionales y actualizan la ficha del adquiriente.
curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{
  "customer_nit": "900123456",
  "customer_name": "Acme SAS",
  "customer_type_doc": "31",
  "customer_person_type": "1",
  "customer_tax_regime": "48",
  "customer_tax_level": "O-13",
  "customer_fiscal_responsibility": "O-13;O-15",
  "customer_email": "pagos@acme.co",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "items": [
    { "description": "Consultoría", "quantity": 10, "unit_price": 150000, "tax_percent": 19 }
  ]
}'

Caso 3 — extranjero, consumidor final

Adquiriente del exterior no identificado. Con is_consumidor_final=true y customer_country es suficiente; la instancia clasifica el documento como exportación (02), deja el IVA en 0 y convierte la moneda a pesos colombianos. Aplica a ventas al detal o a suscripciones de personas en el exterior.

Caso 3 — extranjero, consumidor final

# 3) EXTRANJERO · CONSUMIDOR FINAL
# Un customer_country distinto de CO convierte el tipo 01 en factura de
# exportación (02): el IVA queda en 0 y el total en moneda extranjera se
# convierte a COP con la tasa vigente. Campos mínimos: el indicador, el
# país y las líneas.
curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{
  "is_consumidor_final": true,
  "customer_country": "US",
  "customer_country_name": "United States",
  "currency": "USD",
  "items": [
    { "description": "SaaS subscription", "quantity": 1, "unit_price": 120 }
  ]
}'

Caso 4 — extranjero identificado

Persona jurídica o natural del exterior, con documento propio. Utilice customer_type_doc 50 (NIT de otro país) o 42 (documento de identificación extranjero). Se trata de una factura de exportación (02): sin IVA, con conversión de moneda y, si aplica, delivery_terms_code con el Incoterm.

Caso 4 — extranjero identificado

# 4) EXTRANJERO · IDENTIFICADO (persona jurídica o natural con documento extranjero)
# Factura de exportación (02) con adquiriente identificado. Campos mínimos:
# customer_name, customer_country e items. Campos opcionales de uso frecuente:
# customer_type_doc (50 = NIT de otro país, 42 = documento de identificación
# extranjero) y delivery_terms_code (Incoterm).
curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{
  "customer_name": "Globex LLC",
  "customer_nit": "98-7654321",
  "customer_type_doc": "50",
  "customer_person_type": "1",
  "customer_country": "US",
  "customer_country_name": "United States",
  "customer_address": "1 Market St, San Francisco",
  "customer_email": "ap@globex.com",
  "currency": "USD",
  "delivery_terms_code": "FOB",
  "items": [
    { "description": "Software license", "quantity": 5, "unit_price": 400 }
  ]
}'

Notas crédito y débito (91 y 92)

Las notas se crean mediante el mismo POST /invoices, cambiando invoice_type a 91 (crédito) o 92 (débito). Toda nota corrige una factura anterior; por tanto, correction_invoice_id es obligatorio y debe referirse a una factura de la misma empresa que ya se encuentre ACEPTADA por la DIAN.

No es necesario enviar los datos del adquiriente: la instancia toma el nombre, el NIT y los demás campos de la factura original (una nota hereda siempre el adquiriente de su factura). Es suficiente enviar los ítems y, si se requiere, correction_concept y correction_reason. Una nota crédito no puede superar el saldo aún acreditable de la factura; el excedente se rechaza con 400.

Numeración: las notas llevan la numeración propia del emisor (Res. 000042/2020 art. 7), no la de la resolución de factura. La instancia asigna un consecutivo por tipo, por empresa y por ambiente: NC1, NC2… para notas crédito, ND1, ND2… para notas débito y NAS1, NAS2… para la nota de ajuste al documento soporte (95). Empieza en 1 (o continúa después del mayor número ya emitido con ese prefijo), no consume el rango autorizado de factura ni requiere clave técnica, y el XML no incluye el bloque sts:InvoiceControl. En InvoiceOut, resolution_number, range_from y range_to llegan en null para las notas; el PDF no imprime línea de autorización.

  • invoice_type 91 — nota crédito; 92 — nota débito.
  • invoice_number de una nota: NC<n> / ND<n> / NAS<n> (consecutivo propio, sin resolución DIAN; resolution_number = null).
  • correction_invoice_id es obligatorio y la factura original debe estar ACEPTADA.
  • No es posible emitir una nota a partir de otra nota.
  • correction_concept utiliza la tabla de conceptos según el tipo (crédito o débito).
  • GET /invoices/{id} exhibe credited_amount, remaining_credit y fully_credited de la factura base.

Conceptos de nota crédito (91) — correction_concept

CódigoDescripción
1Devolución parcial de bienes / no aceptación parcial del servicio
2Anulación de factura electrónica
3Rebaja o descuento parcial o total
4Ajuste de precio
5Otros

Conceptos de nota débito (92) — correction_concept

CódigoDescripción
1Intereses
2Gastos por cobrar
3Cambio del valor
4Otros

Solicitud — nota crédito de anulación

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": "inv_01H9A...",
  "correction_concept": "2",
  "correction_reason": "Anulación total de la factura por devolución",
  "items": [
    { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000, "tax_percent": 19 }
  ]
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/invoices", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "invoice_type": "91",
  "correction_invoice_id": "inv_01H9A...",
  "correction_concept": "2",
  "correction_reason": "Anulación total de la factura por devolución",
  "items": [
    { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000, "tax_percent": 19 }
  ]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "invoice_type": "91",
  "correction_invoice_id": "inv_01H9A...",
  "correction_concept": "2",
  "correction_reason": "Anulación total de la factura por devolución",
  "items": [
    { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000, "tax_percent": 19 }
  ]
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/invoices",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/invoices");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "invoice_type" => "91",
    "correction_invoice_id" => "inv_01H9A...",
    "correction_concept" => "2",
    "correction_reason" => "Anulación total de la factura por devolución",
    "items" => [
        [
            "description" => "Consultoría de software",
            "quantity" => 10,
            "unit_price" => 150000,
            "tax_percent" => 19
        ]
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201

{
  "id": "inv_01H9C...",
  "company_id": "cmp_01H8Z...",
  "environment": "PRODUCTION",
  "invoice_number": "NC45",
  "invoice_type": "91",
  "prefix": "NC",
  "cude": "b2c3d4e5f6a7...1a",
  "qr_code": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=b2c3...",
  "dian_status": "SIGNED",
  "correction_invoice_id": "inv_01H9A...",
  "correction_concept": "2",
  "correction_reason": "Anulación total de la factura por devolución",
  "customer_name": "Acme SAS",
  "customer_nit": "900123456",
  "subtotal_amount": "1500000.00",
  "tax_total_amount": "285000.00",
  "total_amount": "1785000.00",
  "currency": "COP",
  "line_items": [
    {
      "line_number": 1,
      "description": "Consultoría de software",
      "quantity": "10.00",
      "unit_price": "150000.00",
      "tax_percent": "19.00",
      "tax_code": "01",
      "line_extension": "1500000.00",
      "tax_amount": "285000.00",
      "unit_code": "HUR"
    }
  ],
  "issue_date": "2026-09-09T09:15:00Z",
  "created_at": "2026-09-09T09:15:00Z"
}

Tipos de operación DIAN (AIU, mandatos, transporte, talonario, notas sin referencia)

Por defecto toda factura de venta es la operación estándar (CustomizationID 10) y la exportación se detecta por el país del adquiriente. Las operaciones especiales del Anexo Técnico 1.9 (tabla 13.1.5) se piden con operation_type y solo se aceptan si la empresa las activó en Configuración → Facturación → Tipos de documento; de lo contrario la respuesta es 400 e indica dónde activarlas. GET /invoices/defaults devuelve enabled_operation_types.

Cada tipo añade un bloque propio. 09 AIU: objeto aiu { mode: construccion | art462_1, contract_object } y en cada ítem aiu_component A, I o U (vacío = costo directo); en construcción el IVA va solo sobre la utilidad, en art462_1 (aseo, vigilancia, temporales) sobre A+I+U, que debe ser al menos el 10 % del contrato. 11 mandatos: line_scheme 0 (ingreso propio) o 1 (ingreso de terceros) en cada ítem, y mandante { nit, dv, type_doc, name } en las líneas de terceros; esa parte se contabiliza en 281510 y no es ingreso propio. 12 transporte de carga (remesas RNDC): line_scheme 1 (remesa) o 0 (servicio adicional) y remesas[] { radicado, consecutivo, flete, quantity, unit KGM | GLL, original_invoice } cuyos fletes suman el valor de la línea.

Tipo 03 (talonario de contingencia): invoice_type 03 con talonario { number, date }; el número debe estar dentro de un rango TALONARIO y no consume la numeración electrónica. Notas sin referencia: invoice_type 91 con operation_type 22, o 92 con 32, sin correction_invoice_id y con note_period_start / note_period_end; sirven para ajustar facturas ya aceptadas como título valor (LGC15 / LGC16). El concepto 2 (anulación) no aplica.

  • operation_type: 10 (estándar, por defecto), 09 AIU, 11 mandatos, 12 transporte de carga (remesas RNDC; el manifiesto de carga no se emite); 22 / 32 notas sin referencia.
  • InvoiceOut incluye operation_type; los documentos anteriores lo devuelven vacío (= 10).
  • Cada tipo se libera por empresa: pruébelo primero en el ambiente de habilitación.

Solicitud — factura de transporte de carga (12)

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": "01",
  "operation_type": "12",
  "customer_nit": "900123456",
  "items": [{
    "description": "Flete Bogotá - Medellín",
    "quantity": 1, "unit_price": 1500000, "tax_percent": 0,
    "is_service": true, "line_scheme": "1",
    "remesas": [{ "radicado": "123456789", "consecutivo": "R-001", "flete": 1500000, "quantity": 12000, "unit": "KGM" }]
  }]
}'

Documento rechazado: corregir y reenviar

Un documento RECHAZADO por la DIAN no existe para la DIAN: no consumió el consecutivo y puede corregirse y reenviarse con el mismo número. GET /invoices/{id} (y /soporte, /payroll) devuelve errors_explained, una lista con cada regla devuelta por la DIAN traducida a causa y corrección sugerida en español: { code, area, cause, fix, message, known }. El mismo bloque llega en el webhook invoice.rejected.

PUT /invoices/{id} acepta el mismo cuerpo de POST /invoices (solo los campos presentes se modifican). Con finalize: true se reconstruye el XML con el constructor correcto para el tipo (01/02/91/92/05/95), se recalcula el CUFE/CUDE/CUDS con la nueva hora de emisión y se firma de nuevo; con send_to_dian: true se transmite en la misma llamada. Solo se admite en dian_status REJECTED, ERROR o DRAFT; un documento SENT, QUEUED o ACCEPTED responde 409 con code invoice_not_correctable, porque la DIAN ya pudo registrar ese número (regla 90).

Regla 90 («Documento procesado anteriormente») en producción: la DIAN ya tenía ese número, normalmente porque el software anterior lo usó después del cambio. miPlata lo resuelve sola: primero consulta su propio CUFE (si la DIAN ya había aceptado este mismo documento, queda ACCEPTED sin cambios); si no, reemite el mismo documento (mismo id) con el siguiente consecutivo libre, nueva hora de firma y nuevo CUFE, y lo reenvía (hasta 5 veces). Recibe invoice.renumbered con invoice_number nuevo y previous_invoice_number, y después el accepted / rejected de siempre. Aplica a 01, 02 y 05; las notas 91/92/95 no se renumeran.

Nómina electrónica: PUT /payroll/{id} con los campos a corregir (periodo, trabajador, salario, días, devengados, deducciones) reconstruye el documento sobre el mismo consecutivo NE (nuevo CUNE), lo firma y lo reenvía. Aplica a 102 y 103 en REJECTED o ERROR.

  • errors_explained: código de regla (FAB, FAD, FAJ, FAK, FAS, FAU, FAX, FAZ, CAF, DAF, NIE…, 90, CTG01), área, causa y corrección sugerida.
  • PUT /invoices/{id}, PUT /soporte/{id}, PUT /payroll/{id}: mismo número, nuevo CUFE/CUDE/CUDS/CUNE, un solo cobro de cupo por transmisión.
  • Webhook invoice.corrected (y credit_note.corrected, debit_note.corrected, payroll.corrected) al reconstruir; después siguen sent / accepted / rejected como de costumbre. invoice.renumbered cuando la regla 90 obligó a usar el siguiente consecutivo.

Corregir el nombre del adquiriente y reenviar

curl -sS -X PUT https://TU-EMPRESA.miplata.co/api/external/invoices/3f2b… \
  -H "X-API-Key: TU_LLAVE" -H "Content-Type: application/json" \
  -d '{ "customer_name": "ACME S.A.S.", "finalize": true, "send_to_dian": true }'

Respuesta 200

{
  "id": "3f2b…",
  "invoice_number": "FE1501",
  "dian_status": "ACCEPTED",
  "cufe": "…nuevo…",
  "errors_explained": null
}

Indisponibilidad de la DIAN (pendiente, tipo 04, plazo de 48 h)

Cuando la DIAN no responde (tiempo de espera agotado, error 5xx, servicio caído) el documento no se pierde ni se convierte en otro tipo: queda firmado, con su número, en dian_status QUEUED, y la instancia lo reintenta siguiendo la cadencia del Anexo técnico 1.9: tres intentos cada 20 segundos y después una verificación cada 30 minutos, hasta transmitirlo. El plazo legal para transmitir es de 48 horas desde que el servicio se restablece; la instancia lo registra en dian_queue.deadline_at y avisa al titular si vence.

GET /invoices/{id} expone dian_queue: { state (PENDING | RETRYING | WAITING_DIAN | DEADLINE_MISSED | SENT | FAILED | CANCELLED), attempts, next_attempt_at, deadline_at, last_attempt_at, last_error, open }. PENDING y RETRYING corresponden a la ráfaga inicial; WAITING_DIAN indica que el documento quedó aparcado y se retoma en la siguiente verificación de 30 minutos; DEADLINE_MISSED señala que venció el plazo de 48 horas sin transmitir —el documento sigue en cola y se transmite en cuanto la DIAN responda, pero el titular recibe un aviso para evaluar el procedimiento de la DIAN para documentos extemporáneos—. SENT, FAILED y CANCELLED son finales (open: false): SENT transmitido, FAILED rechazado por la DIAN o sin XML firmado, CANCELLED retirado de la cola sin transmitir (el documento se reemplazó o anuló antes de que la DIAN respondiera). El webhook invoice.pending_dian se dispara al encolarse; invoice.sent / invoice.accepted / invoice.rejected siguen cuando la DIAN responde. POST /invoices/{id}/retry-now fuerza un intento inmediato.

El mismo mecanismo cubre notas crédito y débito (credit_note.pending_dian, debit_note.pending_dian), documentos soporte (familia invoice, mismos endpoints /invoices/{id}/retry-now) y nómina electrónica (payroll.pending_dian; POST /payroll/{id}/retry-now). El tipo 04 solo existe para facturas de venta y exportación (01/02).

Mientras la DIAN esté caída, su integración no debe reintentar el POST: el documento ya está firmado y numerado en la instancia. Consulte GET /invoices/{id} o espere los webhooks. Un nuevo POST con la misma Idempotency-Key devuelve el mismo documento; sin ella crearía un segundo documento con otro consecutivo.

Tipo 04 (opcional). Si necesita entregar la factura al cliente durante la caída, POST /invoices/{id}/contingency-04 la reemite como InvoiceTypeCode 04 con el mismo prefijo y consecutivo, la firma de nuevo y la deja en dian_status CONTINGENCIA_04 hasta transmitirla (webhook invoice.contingency_04). La DIAN solo acepta un tipo 04 cuya hora de firma cae dentro de una indisponibilidad declarada por ella (regla CTG01); si la rechaza, corríjala con PUT /invoices/{id} indicando invoice_type: "01" y reenvíe con el mismo número.

El tipo 03 (contingencia del facturador, factura de talonario) no se genera automáticamente: es un documento distinto que referencia la factura en papel y solo aplica cuando falla el software del emisor, no la DIAN.

  • QUEUED → reintentos automáticos (3 × 20 s, luego cada 30 min) → SENT / ACCEPTED. Nunca ERROR por una falla de red.
  • dian_queue en GET /invoices/{id}; invoice.pending_dian, invoice.contingency_04 en webhooks.
  • POST /invoices/{id}/retry-now y POST /invoices/{id}/contingency-04 reflejan las acciones «Reintentar ahora» y «Emitir tipo 04» de la aplicación.
  • En Sandbox, «Simular caída de la DIAN» (Configuración → Entorno) reproduce todo el flujo sin tocar la red: QUEUED, dian_queue, webhooks pending_dian, «Reintentar ahora», «Emitir tipo 04» y la transmisión real al desactivarlo.

GET /invoices/{id} de un documento pendiente

{
  "id": "3f2b…",
  "invoice_number": "FE1502",
  "dian_status": "QUEUED",
  "dian_queue": {
    "state": "WAITING_DIAN",
    "attempts": 3,
    "next_attempt_at": "2026-09-30T18:30:00+00:00",
    "deadline_at": "2026-10-02T17:58:12+00:00",
    "last_error": "Request timed out (25s)",
    "open": true
  }
}

Consultar estado, XML, AttachedDocument y PDF

GET /invoices/{id} devuelve el documento con su dian_status y cufe; GET /invoices/{id}/pdf la representación gráfica. Para entregar el documento a un tercero conforme al Anexo técnico 1.9 §6.4, GET /invoices/{id}/attached-document devuelve el contenedor AttachedDocument (UBL 2.1) que envuelve el XML firmado y el ApplicationResponse (acuse) de la DIAN, firmado por el emisor; es el mismo archivo que la instancia adjunta al correo al cliente y al ZIP de descarga. Está disponible para documentos ACCEPTED; para un tipo 04 pendiente el contenedor se entrega sin ApplicationResponse.

  • GET /invoices/{id}: estado, CUFE, errors_explained, dian_queue, amount_paid y balance (saldo pendiente; ver Pagos recibidos fuera de la pasarela).
  • GET /invoices/{id}/attached-document: AttachedDocument firmado (XML + acuse DIAN), application/xml.
  • GET /invoices/{id}/application-response: solo el ApplicationResponse (acuse) de la DIAN, application/xml. Útil para archivar el acuse por separado o verificar el CUFE validado.
  • GET /invoices/{id}/pdf: representación gráfica con código QR.

Emisión masiva (asíncrona)

Cuando se requiera emitir un volumen elevado de documentos, POST /invoices/bulk acepta hasta 500 facturas en una sola solicitud y responde 202 de inmediato con un batch_id. No es necesario esperar a que la DIAN procese cada una: la instancia las encola y las emite en segundo plano, una por una, con el mismo motor y las mismas validaciones de POST /invoices.

Cada elemento del arreglo items contiene un objeto invoice idéntico al cuerpo de POST /invoices (factura 01/02 o nota 91/92) y, de forma opcional, un client_reference: el identificador de origen de ese documento. Ese client_reference es la clave de idempotencia dentro del lote —debe ser único en el batch— y se incluye en cada consulta de estado, para conciliar cada resultado con el pedido de origen. Si un ítem se reintenta por una falla transitoria, no se duplica la factura.

El procesamiento admite falla parcial: el rechazo de un documento no interrumpe a los demás. Consulte GET /invoices/bulk/{batch_id} para observar el avance (status queued → processing → completed), con los contadores total, succeeded, failed y processed, y el detalle de cada ítem con su estado, su CUFE y, si falló, el error exacto.

El cupo del plan se valida por documento en el momento de la emisión: si el cupo se agota a mitad del lote, los documentos restantes fallan con el mismo mensaje 402 en su campo error, sin consumir numeración. Suscríbase a los webhooks invoice_batch.item_processed e invoice_batch.completed para recibir el resultado sin consultas reiteradas.

  • items[] — hasta 500; cada entrada es { client_reference?, invoice }.
  • invoice — el mismo cuerpo de POST /invoices (InvoiceCreateRequest).
  • client_reference — identificador de idempotencia, único dentro del lote; se refleja en cada consulta.
  • Responde 202 con batch_id; el procesamiento ocurre en segundo plano.
  • Falla parcial: cada ítem tiene su propio status y error; el fallo de uno no interrumpe a los demás.
  • order_reference: la misma regla que POST /invoices. Si una factura (01/02) con ese order_reference ya existe —de otro lote o de una emisión individual—, el ítem queda failed con error «409 order_reference_exists: …» (número e invoice_id del documento existente) y no se emite otro.
  • error: mensaje funcional con el código HTTP equivalente (402, 409, 422…), nunca detalles internos.
  • Ritmo: los documentos se emiten en serie por instancia (firma y envío a la DIAN); cuente con varios minutos para un lote de 100. Los contadores del encabezado se calculan a partir de los ítems.
  • El cupo se descuenta por documento emitido; los que no alcanzan cupo fallan con 402 en su campo error.
  • Webhooks: invoice_batch.created, invoice_batch.item_processed, invoice_batch.completed.

Solicitud — encolar un lote

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices/bulk \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "client_reference": "ORD-1001",
      "invoice": {
        "customer_nit": "900123456",
        "items": [
          { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000 }
        ]
      }
    },
    {
      "client_reference": "ORD-1002",
      "invoice": {
        "is_consumidor_final": true,
        "items": [
          { "description": "Venta de mostrador", "quantity": 1, "unit_price": 50000 }
        ]
      }
    }
  ]
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/invoices/bulk", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "items": [
    {
      "client_reference": "ORD-1001",
      "invoice": {
        "customer_nit": "900123456",
        "items": [
          { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000 }
        ]
      }
    },
    {
      "client_reference": "ORD-1002",
      "invoice": {
        "is_consumidor_final": true,
        "items": [
          { "description": "Venta de mostrador", "quantity": 1, "unit_price": 50000 }
        ]
      }
    }
  ]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "items": [
    {
      "client_reference": "ORD-1001",
      "invoice": {
        "customer_nit": "900123456",
        "items": [
          { "description": "Consultoría de software", "quantity": 10, "unit_price": 150000 }
        ]
      }
    },
    {
      "client_reference": "ORD-1002",
      "invoice": {
        "is_consumidor_final": true,
        "items": [
          { "description": "Venta de mostrador", "quantity": 1, "unit_price": 50000 }
        ]
      }
    }
  ]
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/invoices/bulk",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/invoices/bulk");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "items" => [
        [
            "client_reference" => "ORD-1001",
            "invoice" => [
                "customer_nit" => "900123456",
                "items" => [
                    [
                        "description" => "Consultoría de software",
                        "quantity" => 10,
                        "unit_price" => 150000
                    ]
                ]
            ]
        ],
        [
            "client_reference" => "ORD-1002",
            "invoice" => [
                "is_consumidor_final" => true,
                "items" => [
                    [
                        "description" => "Venta de mostrador",
                        "quantity" => 1,
                        "unit_price" => 50000
                    ]
                ]
            ]
        ]
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 202 — lote aceptado

{
  "batch_id": "bat_01H9A...",
  "status": "queued",
  "total": 2,
  "succeeded": 0,
  "failed": 0,
  "processed": 0,
  "created_at": "2026-09-19T14:03:22Z",
  "completed_at": null
}

Estado de un lote

GET /invoices/bulk/{batch_id} devuelve el encabezado del lote (status y contadores) y el arreglo items con el resultado de cada documento, en el mismo orden de envío (idx). Espere el webhook invoice_batch.completed y consulte este recurso una sola vez para confirmar el resultado; si no tiene webhooks, consúltelo con espera creciente (por ejemplo 30 s, 60 s, 120 s), nunca en un ciclo corto.

Solicitud

curl -sS https://TU-EMPRESA.miplata.co/api/external/invoices/bulk/bat_01H9A... \
  -H "X-API-Key: TU_LLAVE"

Respuesta 200 — lote completado con fallo parcial

{
  "batch_id": "bat_01H9A...",
  "status": "completed",
  "total": 2,
  "succeeded": 1,
  "failed": 1,
  "processed": 2,
  "created_at": "2026-09-19T14:03:22Z",
  "completed_at": "2026-09-19T14:03:40Z",
  "items": [
    {
      "idx": 0,
      "client_reference": "ORD-1001",
      "status": "succeeded",
      "invoice_id": "inv_01H9A...",
      "invoice_number": "SETP990000123",
      "cufe": "a1b2c3d4e5f6...9f",
      "dian_status": "SIGNED",
      "error": null
    },
    {
      "idx": 1,
      "client_reference": "ORD-1002",
      "status": "failed",
      "invoice_id": null,
      "invoice_number": null,
      "cufe": null,
      "dian_status": null,
      "error": "402 Cupo de documentos agotado. Compre un paquete de recargas o renueve su plan en Configuración → Suscripción miPlata para seguir transmitiendo a la DIAN."
    }
  ]
}

Origen del documento

Desde la versión 1.11 cada documento informa de dónde vino. El bloque source acompaña a toda factura, nota y documento soporte en las respuestas de la API: kind indica el canal (manual desde la aplicación, pos desde el punto de venta, shop desde la Tienda de miPlata, api desde una llave API, connection desde una tienda conectada por plugin) y, para connection, connection_id, name y system (woocommerce, tiendanube, mercadolibre, custom) identifican la tienda; para api, name es el nombre de la llave. Las notas crédito y débito heredan el origen del documento que corrigen.

El filtro source de GET /invoices y GET /soporte acepta un kind o conn:<connection_id>. En la aplicación, Ventas y Cartera muestran el mismo origen como etiqueta y el panel Ventas por origen agrega el neto por canal y por conexión.

Bloque source en InvoiceOut

{
  "id": "…", "invoice_number": "FE1501", "dian_status": "ACCEPTED",
  "source": { "kind": "connection", "connection_id": "conn_7f3a…", "name": "Tienda Centro", "system": "woocommerce" }
}

Otros documentos

Documento soporte (05 y 95)

El documento soporte lo emite el comprador cuando adquiere bienes o servicios de un proveedor no obligado a facturar electrónicamente. Es el documento que sustenta el costo o la deducción; por ello, normalmente lo genera el sistema que ya registra las compras.

POST /soporte utiliza el mismo motor de POST /invoices, con el tipo fijado: la misma numeración, el mismo CUDS, la misma firma y la misma transmisión. Si se omite invoice_type, se asume 05. Únicamente admite 05 y 95; para los demás tipos utilice POST /invoices.

Para la nota de ajuste envíe invoice_type 95 y correction_invoice_id con el identificador del documento soporte que se corrige; ese documento debe existir en la misma empresa y ser de tipo 05.

El documento soporte requiere su propia resolución de numeración (prefijo distinto al de venta). El proveedor se informa en los campos customer_*: en este documento, dichos campos identifican a quien suministró el bien o el servicio.

  • invoice_type 05 — documento soporte en adquisiciones a no obligados a facturar.
  • invoice_type 95 — nota de ajuste al documento soporte; requiere correction_invoice_id.
  • Los datos del proveedor se informan en customer_name, customer_nit y customer_type_doc.
  • ds_period — forma de generación del 05: operation (una por operación, predeterminado) o weekly (acumulado semanal, máximo 6 días atrás). El DS debe quedar emitido dentro del mes de la compra.
  • En la aplicación, Compras → Emitir documento soporte genera el 05 desde la compra registrada (proveedor, líneas y retenciones de la compra; sin mover inventario ni duplicar el gasto). Si el proveedor parece obligado a facturar (responsable de IVA o NIT de empresa) solo se advierte.
  • GET /soporte devuelve únicamente 05 y 95; no se mezcla con el listado de ventas.
  • GET /invoices/{id} y /invoices/{id}/pdf también aplican a estos documentos.

Conceptos de nota de ajuste al documento soporte (95) — correction_concept

CódigoDescripción
1Devolución parcial de bienes / no aceptación parcial del servicio
2Anulación del documento soporte
3Rebaja o descuento parcial o total
4Ajuste de precio
5Otros

Solicitud — documento soporte 05

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/soporte \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "invoice_type": "05",
  "customer_name": "Juan Pérez",
  "customer_nit": "1020304050",
  "customer_type_doc": "13",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "payment_form": "1",
  "payment_means_code": "10",
  "items": [
    { "description": "Servicio de mantenimiento", "quantity": 1, "unit_price": 350000, "tax_percent": 0, "tax_code": "01" }
  ]
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/soporte", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "invoice_type": "05",
  "customer_name": "Juan Pérez",
  "customer_nit": "1020304050",
  "customer_type_doc": "13",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "payment_form": "1",
  "payment_means_code": "10",
  "items": [
    { "description": "Servicio de mantenimiento", "quantity": 1, "unit_price": 350000, "tax_percent": 0, "tax_code": "01" }
  ]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "invoice_type": "05",
  "customer_name": "Juan Pérez",
  "customer_nit": "1020304050",
  "customer_type_doc": "13",
  "customer_city_code": "11001",
  "customer_department_code": "11",
  "payment_form": "1",
  "payment_means_code": "10",
  "items": [
    { "description": "Servicio de mantenimiento", "quantity": 1, "unit_price": 350000, "tax_percent": 0, "tax_code": "01" }
  ]
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/soporte",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/soporte");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "invoice_type" => "05",
    "customer_name" => "Juan Pérez",
    "customer_nit" => "1020304050",
    "customer_type_doc" => "13",
    "customer_city_code" => "11001",
    "customer_department_code" => "11",
    "payment_form" => "1",
    "payment_means_code" => "10",
    "items" => [
        [
            "description" => "Servicio de mantenimiento",
            "quantity" => 1,
            "unit_price" => 350000,
            "tax_percent" => 0,
            "tax_code" => "01"
        ]
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201

{
  "id": "inv_01H9D...",
  "company_id": "cmp_01H8Z...",
  "environment": "PRODUCTION",
  "invoice_number": "DS990000012",
  "invoice_type": "05",
  "prefix": "DS",
  "cuds": "c3d4e5f6a7b8...2b",
  "qr_code": "https://catalogo-vpfe.dian.gov.co/document/searchqr?documentkey=c3d4...",
  "dian_status": "SIGNED",
  "customer_name": "Juan Pérez",
  "customer_nit": "1020304050",
  "customer_type_doc": "13",
  "subtotal_amount": "350000.00",
  "tax_total_amount": "0.00",
  "total_amount": "350000.00",
  "currency": "COP",
  "line_items": [
    {
      "line_number": 1,
      "description": "Servicio de mantenimiento",
      "quantity": "1.00",
      "unit_price": "350000.00",
      "tax_percent": "0.00",
      "tax_code": "01",
      "line_extension": "350000.00",
      "tax_amount": "0.00",
      "unit_code": "EA",
      "is_service": false,
      "tax_excluded": false
    }
  ],
  "issue_date": "2026-09-09T11:00:00Z",
  "created_at": "2026-09-09T11:00:00Z"
}

Nómina electrónica (102 y 103)

La nómina electrónica suele originarse en otro sistema (gestión humana o contabilidad); por ello la API expone el ciclo completo: crear, firmar, transmitir, consultar el estado y descargar el PDF.

Con los valores predeterminados (save_only false, send_to_dian true), una sola solicitud crea, firma y transmite. Si requiere revisión previa, envíe save_only true y, posteriormente, invoque /payroll/{id}/sign y /payroll/{id}/send-to-dian cuando corresponda.

Si no se envían earnings ni deductions, la instancia liquida el período con las reglas vigentes: devengado básico según los días laborados, auxilio de transporte legal hasta 2 SMLMV, deducciones de ley del trabajador (4 % de salud y 4 % de pensión, más el Fondo de Solidaridad Pensional desde 4 SMLMV) y la retención en la fuente por el procedimiento 1 (art. 383 E.T.) cuando la empresa es agente retenedor. El IBC respeta el piso de 1 SMLMV, el tope de 25 SMLMV, el 70 % del salario integral y el exceso del 40 % de pagos no salariales (Ley 1393/2010). Por el lado del empleador se aplican la exoneración del art. 114-1 E.T. (salud 8,5 %, SENA e ICBF para trabajadores con menos de 10 SMLMV), la clase ARL y los tipos de cotizante 19, 21, 23 y 51 con sus reglas de salud, pensión y parafiscales. Para nóminas con horas extra, incapacidades, auxilios o embargos, envíe los bloques completos con la estructura de las tablas siguientes.

El detalle de esa liquidación (retención paso a paso por el procedimiento 1, motivos de ajuste del IBC y origen de la decisión del art. 114-1: manual, RUT, conteo de trabajadores o sin confirmar) se consulta en la aplicación, en Nómina → Ver cálculo, y queda en la representación gráfica. La API devuelve el documento con sus totales (accrued_total, deductions_total, total); el XML firmado conserva cada devengado y deducción.

La nómina de ajuste (103) exige replaces_payroll_id referido a una nómina ya ACEPTADA por la DIAN, y adjustment_type 1 (reemplazar) o 2 (eliminar).

No existe una operación de borrado: una nómina 102 aceptada no se edita ni se elimina de forma directa. Para corregirla se emite una 103 tipo 1 (reemplazar) y para anularla una 103 tipo 2 (eliminar), siempre con referencia al CUNE original. Es el mecanismo legal de cancelación (Resolución DIAN 000013 de 2021, artículo 17). Debe transmitirse dentro de los diez (10) primeros días del mes siguiente a la detección del error (artículo 8); vencido ese plazo, la API emite el documento de todas formas y la respuesta incluye un campo advisory que lo recuerda.

  • document_type 102 — nómina individual; 103 — nómina individual de ajuste.
  • El período se informa en period_start y period_end (AAAA-MM-DD); worked_days entre 1 y 31.
  • employee_salary es el salario base en pesos colombianos, como texto, para conservar los decimales.
  • El estado efectivo lo entrega GET /payroll/{id}/status: consulta de nuevo a la DIAN por CUNE y persiste la respuesta.
  • Cada nómina transmitida consume un documento del plan, en las mismas condiciones que una factura.

Cuerpo de la solicitud (PayrollCreateRequest)

CampoTipoReq.Descripción
document_typestring102 = Nómina individual (predeterminado), 103 = Nómina individual de ajuste.
prefixstringPrefijo de la resolución de nómina electrónica. Valor predeterminado: NE (máximo 10 caracteres).
employee_idstringIdentificador del trabajador guardado en la instancia. Cuando se envía, el resto de los datos del trabajador (tipo de cotizante, clase ARL, régimen de salud, art. 387) se toman de la ficha y los campos employee_* del cuerpo solo los sobrescriben.
employee_codestringCódigo interno del trabajador (CodigoTrabajador).
employee_doc_typestringTipo de documento de identidad del trabajador. Valor predeterminado: 13 (cédula de ciudadanía).
employee_docstringSíNúmero de documento de identidad del trabajador.
employee_first_namestringSíPrimer nombre del trabajador.
employee_surnamestringSíPrimer apellido del trabajador.
employee_second_surnamestringSegundo apellido.
employee_other_namesstringOtros nombres.
employee_salarystringSíSalario base en pesos colombianos, como texto, para conservar los decimales.
employee_contract_typestringTipo de contrato DIAN. Valor predeterminado: 1 (término fijo).
employee_integral_salarybooltrue si el trabajador tiene salario integral.
employee_high_risk_pensionbooltrue cuando aplica pensión de alto riesgo (10 puntos adicionales a cargo del empleador, Decreto 2090/2003).
employee_pensionedbooltrue si el trabajador ya es pensionado: no cotiza a pensión ni al FSP.
employee_health_regimestringRégimen de salud según ADRES/BDUA: contributory o subsidized. Un subsidiado de tiempo completo cotiza salud igual (Res. 611/2026 art. 4.4); solo el tipo 51 (tiempo parcial) no cotiza salud.
employee_worker_typestringTipo de cotizante PILA: 01 dependiente (predeterminado), 19 aprendiz en etapa lectiva (solo salud), 21 estudiante residente en salud, 23 practicante Decreto 055/2015 (solo ARL), 51 tiempo parcial (IBC por semanas, sin salud). Se toma de la ficha del trabajador cuando se omite.
employee_arl_classstringClase de riesgo ARL 1–5 (Decreto 1772/1994: 0,522 % · 1,044 % · 2,436 % · 4,350 % · 6,960 %). Se toma de la ficha o de la empresa cuando se omite.
contract_start_datestringFecha de ingreso (AAAA-MM-DD). Necesaria para las primas y cesantías proporcionales y para el cálculo de vacaciones.
contract_end_datestringFecha de retiro (AAAA-MM-DD). Solo en la nómina del mes en que el trabajador se retira; activa la liquidación definitiva.
period_startstringSíFecha de inicio del período, en formato AAAA-MM-DD.
period_endstringSíFecha de fin del período, en formato AAAA-MM-DD.
payment_datestringFecha de pago, en formato AAAA-MM-DD.
settlement_startstringInicio del período de liquidación (AAAA-MM-DD), cuando difiere del período de pago (p. ej. quincenas: 1–15 y 16–30 de 15 días comerciales cada una).
settlement_endstringFin del período de liquidación (AAAA-MM-DD).
worked_daysintDías laborados (1–31). Valor predeterminado: 30. Febrero cuenta 30 días comerciales (CST art. 189).
worked_timestringTiempoLaborado (DIAN 8.4.1). Se calcula desde la fecha de ingreso cuando se omite.
pay_by_daybooltrue cuando el trabajador se paga por día (jornal). Con daily_rate el básico es daily_rate × worked_days.
daily_ratestringValor del día, en pesos, para pay_by_day.
transport_allowancestringAuxilio de transporte del mes. Si se omite se aplica el legal (2026: 249.095) cuando el salario es ≤ 2 SMLMV y grant_transport no es false.
grant_transportboolfalse para no pagar auxilio de transporte (trabajo remoto, transporte suministrado).
overtimearrayHoras extra y recargos [{ kind: HED|HEN|HRN|HEDDF|HRDDF|HENDF|HRNDF, hours, work_date }]. Se valoran con la hora ordinaria y los recargos vigentes en cada work_date (Ley 2466/2025: recargo nocturno desde las 19:00; dominical 90 % desde julio 2026).
bonus_amountstringBonificación salarial del mes (hace base de prestaciones y PILA). Para una bonificación no salarial use earnings.bonificaciones.no_salarial; la parte que exceda el 40 % de la remuneración total entra al IBC (Ley 1393/2010 art. 30).
withholding_amountstringRetención en la fuente a cargo del trabajador. Si se omite se calcula con el procedimiento 1 (art. 383 E.T.) cuando la empresa es agente retenedor (responsabilidad 07 en el RUT): INCR, deducciones art. 387, 25 % exento, tope 40 % / 790 UVT anuales y tabla de 95 UVT.
withholding_dependentsboolArt. 387 E.T.: deducción por dependientes del 10 % (máximo 32 UVT al mes). Nunca se asume; se toma de la ficha del trabajador cuando se omite.
withholding_prepaid_healthstringArt. 387 E.T.: pagos del mes a medicina prepagada (máximo 16 UVT).
withholding_housing_intereststringArt. 387 E.T.: intereses de vivienda del mes (máximo 100 UVT).
withholding_voluntary_afcstringAportes AFC o pensión voluntaria del mes descontados por nómina (arts. 126-1 y 126-4 E.T.): renta exenta.
payment_method_codestringMedio de pago DIAN. Valor predeterminado: 47 (transferencia débito bancaria). Consulte la tabla de medios de pago.
bank_namestringEntidad financiera, cuando el pago se realiza por transferencia.
account_typestringTipo de cuenta.
account_numberstringNúmero de cuenta.
earningsobjectBloque detallado de devengados. Opcional; consulte la estructura más adelante.
deductionsobjectBloque detallado de deducciones. Opcional; consulte la estructura más adelante.
replaces_payroll_idstringIdentificador de la nómina electrónica ACEPTADA que se ajusta. Obligatorio para el tipo 103.
adjustment_typestring1 = Reemplazar, 2 = Eliminar. Obligatorio para el tipo 103.
save_onlyboolSi es true, el documento se guarda como borrador, sin firma ni transmisión.
send_to_dianboolSi es true (valor predeterminado), el documento se transmite después de firmarse.

earnings — claves reconocidas

ClaveFormaDescripción
basico{ dias_trabajados, sueldo_trabajado }Devengado básico. Si se omite, se calcula según los días laborados.
transporte{ auxilio_transporte, viatico_s, viatico_ns }Auxilio de transporte y viáticos (salariales / no salariales).
teletrabajovalorAuxilio de teletrabajo.
bonif_retirovalorBonificación por retiro.
indemnizacionvalorIndemnización.
reintegrovalorReintegro.

deductions — claves reconocidas

ClaveFormaDescripción
salud{ porcentaje, valor_base, deduccion }Aporte a salud. Valor predeterminado: 4 % del salario.
fondo_pension{ porcentaje, valor_base, deduccion }Aporte a pensión. Valor predeterminado: 4 % del salario.
fondo_sp{ porcentaje, deduccion_sp, porcentaje_sub, deduccion_sub }Fondo de solidaridad pensional y subsistencia.
retencion_fuentevalorRetención en la fuente.
ica / afc / cooperativavalorICA, ahorro AFC, cooperativa.
embargo_fiscal / deudavalorEmbargos y deudas.
plan_complementarios / educacion / pension_voluntariavalorOtras deducciones de valor simple.

Solicitud mínima — crea, firma y transmite

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/payroll \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "document_type": "102",
  "prefix": "NE",
  "employee_doc_type": "13",
  "employee_doc": "1020304050",
  "employee_first_name": "Juan",
  "employee_surname": "Pérez",
  "employee_salary": "1750905",
  "employee_contract_type": "1",
  "period_start": "2026-09-01",
  "period_end": "2026-09-30",
  "payment_date": "2026-09-30",
  "worked_days": 30,
  "payment_method_code": "47",
  "send_to_dian": true
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/payroll", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "document_type": "102",
  "prefix": "NE",
  "employee_doc_type": "13",
  "employee_doc": "1020304050",
  "employee_first_name": "Juan",
  "employee_surname": "Pérez",
  "employee_salary": "1750905",
  "employee_contract_type": "1",
  "period_start": "2026-09-01",
  "period_end": "2026-09-30",
  "payment_date": "2026-09-30",
  "worked_days": 30,
  "payment_method_code": "47",
  "send_to_dian": true
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "document_type": "102",
  "prefix": "NE",
  "employee_doc_type": "13",
  "employee_doc": "1020304050",
  "employee_first_name": "Juan",
  "employee_surname": "Pérez",
  "employee_salary": "1750905",
  "employee_contract_type": "1",
  "period_start": "2026-09-01",
  "period_end": "2026-09-30",
  "payment_date": "2026-09-30",
  "worked_days": 30,
  "payment_method_code": "47",
  "send_to_dian": true
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/payroll",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/payroll");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "document_type" => "102",
    "prefix" => "NE",
    "employee_doc_type" => "13",
    "employee_doc" => "1020304050",
    "employee_first_name" => "Juan",
    "employee_surname" => "Pérez",
    "employee_salary" => "1750905",
    "employee_contract_type" => "1",
    "period_start" => "2026-09-01",
    "period_end" => "2026-09-30",
    "payment_date" => "2026-09-30",
    "worked_days" => 30,
    "payment_method_code" => "47",
    "send_to_dian" => true
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201

{
  "id": "pay_01H9B...",
  "company_id": "cmp_01H8Z...",
  "environment": "PRODUCTION",
  "document_type": "102",
  "prefix": "NE",
  "consecutive": 45,
  "cune": "f0e1d2c3b4a5...12",
  "is_adjustment": false,
  "period_start": "2026-09-01",
  "period_end": "2026-09-30",
  "employee_doc": "1020304050",
  "employee_full_name": "Juan Pérez",
  "worked_days": 30,
  "accrued_total": "2000000.00",
  "deductions_total": "140072.00",
  "total": "1859928.00",
  "dian_status": "SENT",
  "issue_date": "2026-09-30T10:00:00Z",
  "created_at": "2026-09-30T10:00:00Z"
}

Nómina con devengados y deducciones

Cuando el período incluye auxilios, retenciones u otros conceptos, envíe los bloques earnings y deductions. Cada clave omitida se calcula o se deja en cero; salud y pensión se emiten siempre (4 %, valor predeterminado).

Solicitud con bloques completos

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/payroll \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "document_type": "102",
  "prefix": "NE",
  "employee_doc_type": "13",
  "employee_doc": "1020304050",
  "employee_first_name": "Juan",
  "employee_surname": "Pérez",
  "employee_salary": "2000000",
  "period_start": "2026-09-01",
  "period_end": "2026-09-30",
  "worked_days": 30,
  "earnings": {
    "basico": { "dias_trabajados": 30, "sueldo_trabajado": "2000000" },
    "transporte": { "auxilio_transporte": "200000" }
  },
  "deductions": {
    "salud":        { "porcentaje": "4.00", "valor_base": "2000000", "deduccion": "80000" },
    "fondo_pension":{ "porcentaje": "4.00", "valor_base": "2000000", "deduccion": "80000" },
    "retencion_fuente": "0"
  }
}'

Eventos RADIAN (eventos 030-034 de compra)

Como adquiriente puede responder ante la DIAN a la factura de un proveedor: acusar recibo, aceptar o reclamar. La factura se identifica por su CUFE —el mismo código de 96 caracteres impreso en cada documento—, de modo que no es necesario importarla previamente: la instancia la consulta ante la DIAN, la registra para su empresa y firma el evento.

POST /events utiliza el mismo motor de eventos de la aplicación: calcula el CUDE, construye el UBL ApplicationResponse, lo firma con el certificado de la empresa y lo transmite. La empresa se resuelve a partir de la llave, nunca del cuerpo; por tanto, únicamente pueden emitirse eventos de la propia instancia.

El evento 031 (Reclamo) exige rejection_reason. GET /events/{cufe} consulta ante la DIAN el historial de eventos, únicamente para una factura que ya pertenezca a su empresa.

Orden obligatorio (Decreto 1154 de 2020, Res. 85 de 2022): 030 primero; 032 requiere 030; 031 y 033 requieren 030 y 032, y son excluyentes entre sí. Ningún evento se envía dos veces. La instancia lo valida antes de firmar, de modo que un evento fuera de orden recibe 409 (event_out_of_order con missing, event_already_sent o event_conflict) en lugar de un rechazo de la DIAN, y no consume cupo.

034 (aceptación tácita) lo registra el emisor de la factura cuando el adquiriente no reclamó en 3 días hábiles; desde el lado del adquiriente se rechaza con 422 event_not_allowed.

receiver_person (opcional, en 030 y 032): la persona natural que recibió la factura o el bien { id_type, id_number, first_name, family_name, job_title, department }. Va en el bloque Person del ApplicationResponse; si se omite se usa el representante legal registrado de la empresa.

  • 030 — Acuse de recibo de la Factura Electrónica de Venta.
  • 031 — Reclamo (rechazo); requiere rejection_reason; después de 030 y 032; excluye 033.
  • 032 — Recibo del bien o prestación del servicio; después de 030.
  • 033 — Aceptación expresa; después de 030 y 032; excluye 031.
  • 034 — Aceptación tácita: solo el emisor; aquí 422 event_not_allowed.
  • Cada evento transmitido consume un documento del plan, en las mismas condiciones que una factura.

Cuerpo de POST /events

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/events \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "cufe": "a1b2c3d4e5f6...(96 hex)",
  "event_code": "030",
  "receiver_person": {
    "id_type": "13",
    "id_number": "1020304050",
    "first_name": "Laura",
    "family_name": "Gómez Ruiz",
    "job_title": "Jefe de compras"
  }
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/events", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "cufe": "a1b2c3d4e5f6...(96 hex)",
  "event_code": "030",
  "receiver_person": {
    "id_type": "13",
    "id_number": "1020304050",
    "first_name": "Laura",
    "family_name": "Gómez Ruiz",
    "job_title": "Jefe de compras"
  }
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "cufe": "a1b2c3d4e5f6...(96 hex)",
  "event_code": "030",
  "receiver_person": {
    "id_type": "13",
    "id_number": "1020304050",
    "first_name": "Laura",
    "family_name": "Gómez Ruiz",
    "job_title": "Jefe de compras"
  }
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/events",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/events");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "cufe" => "a1b2c3d4e5f6...(96 hex)",
    "event_code" => "030",
    "receiver_person" => [
        "id_type" => "13",
        "id_number" => "1020304050",
        "first_name" => "Laura",
        "family_name" => "Gómez Ruiz",
        "job_title" => "Jefe de compras"
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201

{
  "cufe": "a1b2c3d4e5f6...(96 hex)",
  "event_code": "030",
  "event_name": "Acuse de recibo de la Factura Electrónica de Venta",
  "accepted": true,
  "dian_response": { "...": "respuesta de la DIAN" },
  "error": null
}

Cuenta y operación

Plan, cupo y precios

El plan incluye la aplicación completa, el certificado de firma, la API (sin cargo por llamada) y un cupo de documentos por periodo. Cada documento transmitido a la DIAN en producción —factura, nota, documento soporte, evento RADIAN, nómina— consume una unidad, desde la aplicación o por API; un reintento por indisponibilidad de la DIAN no consume de nuevo. Al agotarse el cupo se usan los paquetes de recarga, el único costo variable. Sandbox no consume cupo.

GET /usage devuelve el saldo: documents.total_remaining = included_remaining (cupo del plan) + pack_remaining (recargas vigentes); blocked es true cuando la próxima transmisión en producción se rechazaría. Consúltelo al iniciar y después de emitir; no hace falta sondearlo.

Sin cupo, toda ruta que transmite a la DIAN (POST /invoices, /soporte, /events, nómina y su send-to-dian) y los cobros pay_and_invoice responden 402 {"detail": "Cupo de documentos agotado. Compre un paquete de recargas o renueve su plan en Configuración → Suscripción miPlata para seguir transmitiendo a la DIAN.", "code": "quota_blocked"}; nada se crea ni consume numeración. Las recargas y la renovación se compran en Configuración → Suscripción miPlata (Planes · Recargas de documentos · Complementos) y GET /usage las refleja de inmediato.

Cobros (Sistema Pagos): no consumen cupo; tarifa pública de 2,5 % + $ 900 por transacción recaudada; retiros a su banco: 4 retiros gratis al mes; de ahí en adelante $ 2.500 + IVA por retiro. Detalle en Sistema Pagos.

Planes (aplicación completa + API + certificado) y cupo incluido

PlanPrecioDocumentos incluidos por periodo
Plan 3 Meses$ 107.700 (3 meses, $ 35.900/mes)250 documentos
Plan 12 Meses$ 358.800 (12 meses, $ 29.900/mes)1.000 documentos

Paquetes de recarga (documentos adicionales)

PaquetePrecioDocumentosPrecio por documentoVigencia
Recarga $100.000$ 100.0002.000$ 5012 meses
Recarga $500.000$ 500.00012.500$ 4012 meses
Recarga $1.000.000$ 1.000.00027.000$ 3712 meses

GET /usage

curl -sS -X GET https://TU-EMPRESA.miplata.co/api/external/usage \
  -H "X-API-Key: TU_LLAVE"
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/usage", {
  method: "GET",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
  },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/usage",
    method="GET",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/usage");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 200

{
  "plan": {
    "code": "profesional",
    "name": "Profesional",
    "active": true,
    "current_period_end": "2026-10-01"
  },
  "documents": {
    "included_remaining": 320,
    "pack_remaining": 0,
    "total_remaining": 320,
    "blocked": false,
    "updated_at": "2026-09-19T14:03:22Z"
  }
}

Webhooks de salida

Si su sistema debe recibir notificaciones cuando se emite un documento o cuando la DIAN lo acepta o lo rechaza, registre una URL HTTPS para la recepción de eventos. Cada evento se envía como POST JSON con dos firmas HMAC-SHA256 (prefijo sha256=) calculadas con el secreto del webhook: X-MiPlata-Signature-V2 sobre «X-MiPlata-Timestamp + "." + cuerpo» (recomendada: rechace la entrega si el timestamp difiere más de 5 minutos de su reloj, así una entrega capturada no puede repetirse) y X-MiPlata-Signature sobre el cuerpo solo (compatibilidad con integraciones anteriores a v1.17). Verifique la firma antes de dar por válido el cuerpo.

Puede administrarlos desde su instancia → Configuración → Webhooks, o por API: POST /webhooks registra la URL y devuelve el secreto de firma una sola vez; GET /webhooks los lista; PATCH /webhooks/{id} modifica el nombre, la URL, los eventos o el estado de activación; POST /webhooks/{id}/rotate genera un secreto nuevo; y DELETE /webhooks/{id} lo elimina. Únicamente una llave raíz administra webhooks; un token de acceso personal (PAT) recibe 403.

Cada evento tiene la forma familia.acción. La familia es el tipo de documento (invoice para facturas de venta 01/02/03/04 y documento soporte 05/95, credit_note para notas crédito 91, debit_note para notas débito 92 y payroll para nómina electrónica 102/103) y la acción es el momento del ciclo: created al emitirse, sent al transmitirse, accepted o rejected según la respuesta de la DIAN. La nómina emite solo payroll.pending_dian, payroll.corrected, payroll.accepted y payroll.rejected (la nómina no tiene acciones created ni sent). Tres acciones más cubren la indisponibilidad de la DIAN y la corrección: pending_dian cuando la DIAN no respondió y el documento quedó en cola, contingency_04 cuando se reemitió como tipo 04, y corrected cuando un documento rechazado se reconstruyó sobre el mismo número. El payload de rejected incluye errors_explained. La acción paid (solo invoice.paid) avisa cuando el adquiriente pagó por el enlace de pago del documento, con el bloque payment (y quote_id si pagó una cotización). Los cobros creados con llave API emiten checkout.paid, checkout.invoiced y checkout.credit_note (ids, estado, montos y order_reference; nunca datos del comprador). Suscríbase con una lista separada por comas: eventos concretos (invoice.accepted), familias completas con familia.* (invoice.*, credit_note.*, debit_note.*, payroll.*, checkout.*, invoice_batch.*, maintenance.*) o todo con *. Un patrón familia.* incluye también los eventos que esa familia incorpore más adelante; un nombre desconocido responde 422 con la lista válida.

La emisión masiva incorpora la familia invoice_batch: invoice_batch.created cuando se acepta el lote, invoice_batch.item_processed por cada documento resuelto (con el bloque item: su idx, client_reference, estado, CUFE y error), e invoice_batch.completed cuando concluye el lote. Suscríbase a invoice_batch.* para seguir un lote sin consultas reiteradas.

Además, el evento maintenance.scheduled avisa de una ventana de mantenimiento anunciada, con sus horas (starts_at, ends_at en UTC) y su alcance (blocks_api: all, writes o none). Llega con la misma firma y al mismo tiempo que el correo al titular de la cuenta.

Si el servidor de destino no responde 2xx en 8 segundos, la entrega se reintenta cinco veces (seis intentos en total): 30 s, 2 min, 10 min, 1 h y 6 h después de cada fallo. Cada reintento lleva el mismo X-MiPlata-Delivery, un X-MiPlata-Timestamp nuevo (y por tanto firmas nuevas) y X-MiPlata-Attempt con el número de intento: deduplique por X-MiPlata-Delivery y responda 2xx a una entrega que ya procesó. La URL debe ser HTTPS pública: no se aceptan localhost ni redes privadas (se rechaza con 422).

  • Acciones: created (al emitir), sent, accepted, rejected (según la DIAN), pending_dian (DIAN no disponible, en cola), contingency_04 (reemitida tipo 04), corrected (rechazada y corregida sobre el mismo número), renumbered (solo invoice: la DIAN ya tenía el número —regla 90— y miPlata la reemitió con el siguiente consecutivo; incluye previous_invoice_number), paid (solo invoice: pagada por su enlace de pago, un cobro o un pago registrado fuera de la pasarela; payment.provider dice cuál).
  • Familias: invoice.* (01/02/03/04/05/95), credit_note.* (91), debit_note.* (92), payroll.* (102/103: pending_dian, corrected, accepted, rejected), invoice_batch.* (emisión masiva), checkout.* (cobros por llave API), maintenance.scheduled (aviso de mantenimiento).
  • Catálogo completo: invoice.created, invoice.sent, invoice.accepted, invoice.rejected, invoice.pending_dian, invoice.contingency_04, invoice.corrected, invoice.renumbered, invoice.paid; credit_note.created, credit_note.sent, credit_note.accepted, credit_note.rejected, credit_note.pending_dian, credit_note.corrected; debit_note.created, debit_note.sent, debit_note.accepted, debit_note.rejected, debit_note.pending_dian, debit_note.corrected; payroll.pending_dian, payroll.corrected, payroll.accepted, payroll.rejected; checkout.paid, checkout.invoiced, checkout.credit_note; invoice_batch.created, invoice_batch.item_processed, invoice_batch.completed; maintenance.scheduled.
  • Encabezados: X-MiPlata-Event, X-MiPlata-Delivery (clave de deduplicación), X-MiPlata-Attempt, X-MiPlata-Timestamp (Unix, segundos), X-MiPlata-Signature-V2 (timestamp.cuerpo) y X-MiPlata-Signature (solo cuerpo, heredada).
  • Reintentos: 6 intentos en total (30 s, 2 min, 10 min, 1 h, 6 h).
  • El secreto de firma se exhibe una sola vez, al crear o rotar el webhook.
  • Únicamente una llave raíz administra webhooks; un PAT recibe 403.

Cuerpo de POST /webhooks

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/webhooks \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "erp-produccion",
  "url": "https://tu-erp.com/webhooks/miplata",
  "events": "invoice.accepted,credit_note.*"
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/webhooks", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "name": "erp-produccion",
  "url": "https://tu-erp.com/webhooks/miplata",
  "events": "invoice.accepted,credit_note.*"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "name": "erp-produccion",
  "url": "https://tu-erp.com/webhooks/miplata",
  "events": "invoice.accepted,credit_note.*"
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/webhooks",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/webhooks");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "name" => "erp-produccion",
    "url" => "https://tu-erp.com/webhooks/miplata",
    "events" => "invoice.accepted,credit_note.*"
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201 — el secreto se exhibe una sola vez

{
  "id": "wh_01H9A...",
  "name": "erp-produccion",
  "url": "https://tu-erp.com/webhooks/miplata",
  "events": "invoice.accepted,credit_note.created,credit_note.accepted,credit_note.rejected,credit_note.sent",
  "is_active": true,
  "created_at": "2026-09-19T14:03:22Z",
  "last_status": null,
  "last_error": null,
  "last_delivered_at": null,
  "secret": "el-secreto-de-firma-se-exhibe-una-sola-vez"
}

Entrega de un webhook

Así llega cada evento a su URL. Calcule HMAC-SHA256(secreto, X-MiPlata-Timestamp + "." + cuerpo en bruto), compárelo en tiempo constante con X-MiPlata-Signature-V2 y rechace timestamps con más de 5 minutos de diferencia antes de procesar el contenido. Las integraciones anteriores que verifican X-MiPlata-Signature (HMAC del cuerpo solo) siguen funcionando, pero no detectan una entrega repetida: migre a la V2.

Guarde X-MiPlata-Delivery de cada entrega procesada y descarte las repetidas: un reintento tras un timeout de su lado llega con el mismo identificador.

Entrega de ejemplo

POST https://tu-erp.com/webhooks/miplata
Content-Type: application/json
X-MiPlata-Event: invoice.accepted
X-MiPlata-Delivery: whd_01H9C...
X-MiPlata-Attempt: 1
X-MiPlata-Timestamp: 1788871211
X-MiPlata-Signature-V2: sha256=5d0a...7e
X-MiPlata-Signature: sha256=9b2f...c1

{
  "event": "invoice.accepted",
  "occurred_at": "2026-09-08T12:40:11Z",
  "data": {
    "invoice_id": "inv_01H9A...",
    "company_id": "cmp_01H8Z...",
    "invoice_number": "SETP990000123",
    "invoice_type": "01",
    "cufe": "a1b2c3d4e5f6...9f",
    "dian_status": "ACCEPTED"
  }
}

Entrega de un evento de lote

Los eventos de emisión masiva (invoice_batch.*) llegan con la misma firma HMAC. item_processed incorpora el bloque item con el resultado de un documento; completed cierra el lote con los contadores finales. Utilícelos para actualizar su tablero sin consultar GET /invoices/bulk/{batch_id} de forma reiterada.

Entrega de ejemplo — invoice_batch.item_processed

POST https://tu-erp.com/webhooks/miplata
X-MiPlata-Event: invoice_batch.item_processed
X-MiPlata-Delivery: dlv_01H9A...
X-MiPlata-Attempt: 1
X-MiPlata-Timestamp: 1788871302
X-MiPlata-Signature-V2: sha256=8e4b...20
X-MiPlata-Signature: sha256=1c9e...

{
  "event": "invoice_batch.item_processed",
  "batch_id": "bat_01H9A...",
  "status": "processing",
  "total": 2,
  "succeeded": 1,
  "failed": 0,
  "processed": 1,
  "item": {
    "idx": 0,
    "client_reference": "ORD-1001",
    "status": "succeeded",
    "invoice_id": "inv_01H9A...",
    "invoice_number": "SETP990000123",
    "cufe": "a1b2c3d4e5f6...9f",
    "dian_status": "SIGNED",
    "error": null
  }
}

Operaciones automáticas

Otras interfaces exigen que el integrador administre de forma manual piezas de la operación fiscal, y por ello las exponen. En esta API no son necesarias: la instancia las resuelve de forma automática. Retirar esa automatización sería un retroceso, no una función adicional. Por esa razón no existen recursos para estas operaciones: no se trata de una omisión, sino de un criterio de diseño.

  • Numeración: se selecciona y se valida el consecutivo vigente en cada emisión. No se solicita el próximo número, pues ello favorece duplicados y saltos. GET /ranges permite consultarlo en modo de solo lectura.
  • Monedas: las tasas de cambio se actualizan de forma automática y se aplican a las facturas de exportación. No existe un recurso para fijar la tasa, a fin de no descuadrar los registros contables.
  • Resoluciones de numeración: se sincronizan de forma automática desde la DIAN. GET /ranges las exhibe; no hay carga manual.
  • Software y certificado DIAN: se aprovisionan y se almacenan cifrados durante el alta. Nunca circulan por una llave de cliente.
  • PDF y plantillas: la representación gráfica de un documento ACEPTADO se genera una vez, en segundo plano al aceptarlo la DIAN, y se sirve desde caché (también para los documentos aceptados antes de existir la caché, que se precalientan automáticamente); la de un borrador o un documento pendiente se genera bajo demanda. No hay plantillas configurables por API.

Seguridad

Disponibilidad, tiempos y mantenimiento

Cada instancia es un contenedor dedicado que se suspende tras cinco minutos sin actividad y se reanuda con la primera solicitud. La primera llamada tras un periodo inactivo tarda más (arranque en frío, normalmente 10 a 30 s; hasta unos 40 s la primera vez después de una actualización de plataforma) y puede recibir 503 con Retry-After: 15 mientras la instancia inicia; las siguientes, mientras la instancia está activa, responden en tiempos de consulta. Si su integración consulta de forma esporádica, cuente con ese primer tiempo en el timeout. No se exige un User-Agent de navegador: cualquier cliente HTTP (curl, requests, urllib, Guzzle…) es aceptado en /api/external/*. La verificación de la llave, GET /health, GET /usage, los catálogos, GET /maintenance, GET /company, GET /invoices (listado y filtros), GET /invoices/{id}, el 422 de page/limit fuera de rango y el PDF de un documento ACEPTADO se responden en el borde de la red y no esperan ese arranque.

Objetivos de servicio (no contractuales salvo acuerdo escrito): disponibilidad mensual de 99,5 % fuera de ventanas anunciadas; con la instancia activa, p95 inferior a 1 s en consultas paginadas (limit ≤ 50) y en GET /invoices/{id}, e inferior a 3 s en /pdf de un documento no aceptado; POST /invoices depende del tiempo de respuesta de la DIAN (típicamente 2 a 10 s). Configure en su cliente un timeout de 60 s para POST y de 30 s para consultas.

Mantenimiento: las actualizaciones se realizan fuera del horario laboral de Colombia (lunes a viernes después de las 19:00, sábados después de las 13:00, domingos). Durante una actualización de plataforma, las consultas de la API (GET) siguen funcionando; únicamente las operaciones de escritura (POST, PUT, PATCH, DELETE) pueden recibir 503 durante unos minutos. Solo un incidente puede motivar una intervención en horario laboral.

Las ventanas programadas con impacto se anuncian por correo al titular de la cuenta y por el webhook maintenance.scheduled; GET /maintenance devuelve en cualquier momento la ventana vigente o la próxima (o null), para consultarla antes de un proceso por lotes.

Durante una ventana, una solicitud afectada recibe 503 con el encabezado Retry-After (segundos, máximo 60) y el cuerpo {"code": "maintenance", "retry_after": …, "maintenance": {"starts_at", "ends_at", …}}. Reintente la misma solicitud tras Retry-After; con Idempotency-Key el reintento no duplica documentos. Un 503 sin code maintenance indica que la instancia está iniciando: reintente del mismo modo.

  • 503 + code maintenance: mantenimiento. Reintente tras Retry-After (≤ 60 s).
  • 503 sin code: arranque de la instancia. Reintente tras Retry-After (15 s).
  • blocks_api en GET /maintenance y en el webhook: all (toda la API), writes (solo escrituras), none (solo aviso).
  • Horario de cambios: lun–vie 19:00–07:00, sáb desde 13:00, domingo (hora de Colombia).

Consultar la próxima ventana

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

Respuesta 200 (null si no hay ventana)

{
  "maintenance": {
    "id": "mw_mf3k2a",
    "phase": "upcoming",
    "starts_at": "2026-10-03T01:00:00.000Z",
    "ends_at": "2026-10-03T01:30:00.000Z",
    "blocks_api": "writes",
    "message_es": "Actualización programada de la plataforma.",
    "message_en": null
  }
}

Límites y errores

Existen cuatro capas de control: la lista obligatoria de direcciones IP de cada llave, el límite propio de cada llave (60 solicitudes por minuto, valor predeterminado, nunca superior al máximo de la instancia que fija miPlata), un tope global por dirección IP en el borde, y un bloqueo temporal ante solicitudes reiteradas con credenciales inválidas (diez intentos fallidos en cinco minutos bloquean esa dirección IP durante quince minutos). La llave se verifica en el borde de la red antes de llegar a su instancia, de modo que una llave inválida, vencida, revocada o fuera de su lista de IP recibe el 403 de inmediato.

Los errores se entregan como JSON con la forma {"detail": "..."} y el código HTTP correspondiente.

Códigos de estado

CódigoSignificado
201Documento creado.
200Consulta correcta. En POST /invoices, /soporte o /payroll con un Idempotency-Key ya utilizado: el documento original (encabezado Idempotent-Replayed: true); no se emite otro.
400Regla de negocio incumplida (por ejemplo, falta correction_invoice_id o el original no está ACEPTADA).
401Falta el encabezado X-API-Key: {"detail": "Not authenticated"}.
402code quota_blocked: se agotó el cupo de documentos del plan; el documento no se creó ni consumió numeración (Sandbox y borradores nunca reciben este 402). code licence_expired: la licencia venció; las consultas (GET) siguen abiertas y toda creación, edición o transmisión —incluidos los cobros— se rechaza. Cuerpo {detail, code, renew_url, export_url, export_until} y encabezados X-MiPlata-Licence: expired y X-MiPlata-Export-Until (fin de la ventana de 60 días para exportar).
403Llave inválida (incluida una con formato imposible), revocada o vencida; llave sin direcciones IP configuradas; dirección IP no permitida; o API desactivada. Cuerpo {"detail": …}. code account_suspended: la cuenta está suspendida y toda ruta, incluidas las consultas y los cobros, se rechaza.
404El documento no existe en esa instancia.
409POST /invoices (01/02): ya existe un documento con ese order_reference. detail incluye code order_reference_exists e invoice_id del existente; no se emitió otro. POST /events: event_already_sent, event_conflict (031 y 033 son excluyentes) o event_out_of_order (con missing).
422Cuerpo inválido: verifique tipos y campos obligatorios; el detalle indica el campo. También: Idempotency-Key reutilizado con un cuerpo distinto (POST /invoices, /soporte, /payroll), tarifa de IVA/INC no legal, tax_code no soportado (02/08/22/34/35), responsabilidad fiscal fuera de O-13/O-15/O-23/O-47/R-99-PN, municipio que no existe o no pertenece al departamento, evento 034 desde el adquiriente (event_not_allowed), o filtro de fecha mal formado.
429Se superó el límite de la llave o el tope por dirección IP. Siempre con Retry-After: segundos hasta que se reinicia la ventana. Reintente después de ese tiempo.
503Mantenimiento (code maintenance) o instancia iniciando. Siempre con Retry-After en segundos (máx. 60 en mantenimiento): reintente la misma solicitud después de ese tiempo. Ver «Disponibilidad y mantenimiento».

Ejemplo de error 400

{
  "detail": "correction_invoice_id is required for credit/debit notes (types 91/92)"
}

Seguridad y buenas prácticas

Cada instancia es un entorno aislado, con su propia base de datos y su propio almacenamiento. Una llave no puede acceder a los datos de otro cliente, porque no sale de su instancia.

  • Conserve el secreto como variable de entorno o en un gestor de secretos; nunca en el repositorio ni en el frontend.
  • Una llave por sistema: así podrá revocar una sin afectar los demás.
  • Registre solo las IP públicas de los servidores que llaman a la API (obligatorio) y use vencimiento cuando sea posible.
  • Rote las llaves de forma periódica (Regenerar) y revoque las que ya no utilice.
  • Verifique siempre la firma HMAC de los webhooks antes de procesarlos.
  • Todo uso queda registrado: la instancia almacena la última utilización de cada llave.

Cobros y tiendas

Cobrar desde su web

Todo se integra con la API de miPlata y su llave API; no integre WAVA directamente. miPlata es socio integrador de WAVA: tiene las credenciales y los webhooks de su tienda, y solo un cobro creado por miPlata queda ligado a su factura. No necesita acceso de desarrollo ni aprobación propia ante WAVA.

Flujo (boletas, reservas, cursos): 1) su sistema reserva el producto o el puesto; 2) POST /payments/checkouts con mode pay_and_invoice, su order_reference, el comprador, las líneas, Idempotency-Key, return_url y expires_in_minutes; 3) redirige al comprador al checkout_url; 4) al pagar, miPlata emite la factura y envía checkout.invoiced (y los invoice.*) a sus webhooks —checkout.paid si usó mode pay—; 5) entrega el producto. Si la reserva vence, POST /payments/checkouts/{id}/cancel.

Confirme siempre por webhook o con GET /payments/checkouts/{id} (respuesta inmediata, sin costo de procesamiento): el regreso del navegador a return_url no prueba el pago. Un pago que llega después de vencer o cancelar el cobro igual se factura y llega con late: true; honre el pedido o emita la nota crédito con POST …/credit-note.

  • Sandbox (Configuración → Entorno) = DIAN habilitación + WAVA de pruebas: misma llave, mismas rutas, sin dinero real.
  • Solo se factura lo que usted cobra con pay_and_invoice o con un enlace de pago; sus facturas manuales siguen siendo manuales.
  • La factura conserva su order_reference: GET /invoices?order_reference=RES-1001 la encuentra.
  • Antes de cobrar, GET /payments/capabilities indica en wava.reason por qué no se puede (y qué hacer).

¿Qué camino uso?

Su casoUse
Venta desde su web o ERP, cobrar y luego facturarPOST /payments/checkouts (pay_and_invoice)
Factura ya emitida, cobrarlaPOST /invoices/{id}/pay-link
Revisar antes de facturar (cotización)POST /invoices con is_quote: true + POST /invoices/{id}/pay-link
Solo cobrar, sin facturaPOST /payments/checkouts (pay)
Efectivo o transferencia ya recibidos (sin pasarela)POST /invoices con payment (nace pagada) o POST /invoices/{id}/payments
WooCommerce, Tiendanube, Mercado LibrePlugin oficial (Conexiones de tienda)

POST /payments/checkouts — boleta con reserva de 15 minutos

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/payments/checkouts \
  -H "X-API-Key: TU_LLAVE" \
  -H "Idempotency-Key: RES-1001" \
  -H "Content-Type: application/json" \
  -d '{
  "mode": "pay_and_invoice",
  "order_reference": "RES-1001",
  "currency": "COP",
  "expires_in_minutes": 15,
  "return_url": "https://tu-sitio.co/compra/gracias",
  "buyer": {
    "invoice_to": "person",
    "doc_type": "13",
    "doc_number": "1020304050",
    "first_name": "Ana",
    "last_name": "Pérez",
    "email": "ana@ejemplo.co",
    "phone": "3001234567",
    "city": "Bogotá"
  },
  "lines": [
    { "description": "Boleta Platea F-12 — Función 15/11 20:00", "quantity": 1, "unit_price": 80000, "tax_percent": 0, "tax_exempt_reason": "excluido", "is_service": true }
  ]
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/payments/checkouts", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "mode": "pay_and_invoice",
  "order_reference": "RES-1001",
  "currency": "COP",
  "expires_in_minutes": 15,
  "return_url": "https://tu-sitio.co/compra/gracias",
  "buyer": {
    "invoice_to": "person",
    "doc_type": "13",
    "doc_number": "1020304050",
    "first_name": "Ana",
    "last_name": "Pérez",
    "email": "ana@ejemplo.co",
    "phone": "3001234567",
    "city": "Bogotá"
  },
  "lines": [
    { "description": "Boleta Platea F-12 — Función 15/11 20:00", "quantity": 1, "unit_price": 80000, "tax_percent": 0, "tax_exempt_reason": "excluido", "is_service": true }
  ]
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "mode": "pay_and_invoice",
  "order_reference": "RES-1001",
  "currency": "COP",
  "expires_in_minutes": 15,
  "return_url": "https://tu-sitio.co/compra/gracias",
  "buyer": {
    "invoice_to": "person",
    "doc_type": "13",
    "doc_number": "1020304050",
    "first_name": "Ana",
    "last_name": "Pérez",
    "email": "ana@ejemplo.co",
    "phone": "3001234567",
    "city": "Bogotá"
  },
  "lines": [
    { "description": "Boleta Platea F-12 — Función 15/11 20:00", "quantity": 1, "unit_price": 80000, "tax_percent": 0, "tax_exempt_reason": "excluido", "is_service": true }
  ]
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/payments/checkouts",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/payments/checkouts");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "mode" => "pay_and_invoice",
    "order_reference" => "RES-1001",
    "currency" => "COP",
    "expires_in_minutes" => 15,
    "return_url" => "https://tu-sitio.co/compra/gracias",
    "buyer" => [
        "invoice_to" => "person",
        "doc_type" => "13",
        "doc_number" => "1020304050",
        "first_name" => "Ana",
        "last_name" => "Pérez",
        "email" => "ana@ejemplo.co",
        "phone" => "3001234567",
        "city" => "Bogotá"
    ],
    "lines" => [
        [
            "description" => "Boleta Platea F-12 — Función 15/11 20:00",
            "quantity" => 1,
            "unit_price" => 80000,
            "tax_percent" => 0,
            "tax_exempt_reason" => "excluido",
            "is_service" => true
        ]
    ]
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201

{
  "id": "chk_9f2c1a7e4b3d6c8a5e1f0d2b",
  "status": "pending",
  "mode": "pay_and_invoice",
  "provider": "wava",
  "currency": "COP",
  "amount": 80000,
  "order_reference": "RES-1001",
  "checkout_url": "https://TU-EMPRESA.miplata.co/p/c/chk_9f2c1a7e4b3d6c8a5e1f0d2b.4be1…",
  "invoice": null,
  "late": false,
  "expires_at": "2026-10-09 17:15:04",
  "created_at": "2026-10-09 17:00:04"
}

Sistema Pagos (WAVA y PayPal)

Bre-B, Nequi y Daviplata se cobran con WAVA a través de miPlata, sin cuenta propia en la pasarela ni credenciales que administrar. Se activa al contratar el plan o en Configuración → Pagos en línea; miPlata registra el comercio, sigue el KYC y activa producción al aprobarse. El entorno de pruebas está disponible desde el primer día. GET /payments/capabilities expone el estado (bloque wava).

Abonos: WAVA recauda y usted retira a su cuenta bancaria cuando quiera. 4 retiros gratis al mes; de ahí en adelante $ 2.500 + IVA por retiro.

Sin costo de activación ni mensualidad: cada cobro recaudado paga la tarifa pública por transacción (ver Plan, cupo y precios). PayPal (USD, EUR, GBP, CAD, CHF, MXN, JPY) es con su propia cuenta: client id, secret y webhook id en Configuración → Pagos en línea; aplica la tarifa de PayPal.

Cobros (checkouts)

Un cobro une un pedido con su factura. Se crea con POST /payments/checkouts autenticado con X-API-Key (su web o ERP) o con una conexión de tienda firmada (plugins). Con llave API: mode pay (solo cobra) o pay_and_invoice (cobra y factura); el cobro pertenece a la llave raíz y sus tokens (PAT), y otra llave no lo ve. Las conexiones de tienda también admiten mode invoice (pedido ya cobrado por la tienda; ver Conexiones de tienda).

La moneda decide el proveedor: COP con WAVA; USD, EUR, GBP, CAD, CHF, MXN y JPY con PayPal, sin conversión. La respuesta es 201 con checkout_url: en COP, el checkout de miPlata en su dominio (Nequi, Daviplata, Bre-B sin salir de la página), que deja de funcionar al vencer o cancelar el cobro. Al confirmarse el pago, la factura se emite en lote en pocos minutos (on_paid auto_send: transmitida a la DIAN; draft: borrador) y la entrega delivery (miplata = correo de miPlata).

Envíe Idempotency-Key por pedido; repetir la solicitud o el order_reference devuelve el mismo cobro (200). Consulte con GET /payments/checkouts/{id} o ?order_reference=; POST …/cancel cancela un cobro no pagado; POST …/credit-note solicita la nota crédito de la factura (total o parcial con amount). Webhooks (llave API): checkout.paid, checkout.invoiced, checkout.credit_note, además de invoice.*.

  • GET/payments/capabilitiesModos, monedas, límites, dominios de retorno, paylinks (enlaces de pago activos) y estado de WAVA (wava.ready / wava.reason).
  • POST/payments/buyer/validateValida al comprador sin crear nada.
  • POST/payments/checkoutsCrea el cobro: 201 con checkout_url.
  • GET/payments/checkouts/{id}Estado, factura y documentos.
  • GET/payments/checkouts?order_reference=El cobro de un pedido.
  • POST/payments/checkouts/{id}/cancelCancela un cobro no pagado.
  • POST/payments/checkouts/{id}/credit-noteNota crédito sobre la factura del cobro.
  • GET/payments/checkouts/{id}/documents/{pdf|xml|zip}PDF, XML firmado o paquete.
  • POST/payments/checkouts/{id}/documentsEnlaces de un solo uso para el comprador (7 días).

Cuerpo de POST /payments/checkouts

CampoTipoReq.Descripción
modestringSípay | pay_and_invoice (llave API). Conexiones: además invoice.
order_referencestringSíSu número de pedido (1–80, [A-Za-z0-9._/#-]); único por llave o conexión.
currencystringSíCOP, USD, EUR, GBP, CAD, CHF, MXN o JPY.
lines[]objectSídescription, quantity, unit_price (sin IVA), tax_code, tax_percent, tax_exempt_reason (exento | excluido), is_service, sku, kind (product | shipping | fee | discount).
buyerobjectSíinvoice_to, doc_type, doc_number, first_name/last_name o company_name, email, phone, city o city_code, department. Ver Datos del comprador.
amountnumberTotal esperado; se contrasta con la suma de líneas.
expires_in_minutesintLlave API: vigencia del cobro, 15–4320 (predeterminado 4320 = 72 h). Úselo igual a su reserva.
return_urlstringhttps a donde vuelve el comprador; el dominio exacto debe estar en Configuración → Claves API → Dominios de retorno (ejemplo.co y www.ejemplo.co son dos entradas; si no, 422 return_url_not_allowed).
on_paid / delivery / invoice_timingstringauto_send | draft; miplata | shop_link | both; after_paid | draft_first.
order_url, buyer_note, withholdings, invoice_typevariosEnlace al pedido, nota, retenciones { tax, iva, ica } (solo mode invoice; ver Retenciones), 02 si está habilitado.

Estados de un cobro

statusSignificado
pendingEsperando el pago.
paidPago confirmado; la factura sale en el siguiente lote.
invoicing / invoicedFactura en curso / emitida (bloque invoice: número, CUFE, documentos).
disputedEl monto pagado no coincide; revisión manual.
failedLa factura no pudo emitirse (invoice_error).
cancelled / expiredCancelado / vencido sin pago. Un pago tardío lo pasa a paid con late: true.
unverified / heldSolo conexiones de tienda (ver Conexiones de tienda).

Errores propios del cobro

HTTPcodeQué hacer
402quota_blockedpay_and_invoice sin cupo: compre recargas (Configuración → Suscripción miPlata).
404api_checkouts_offCobros por llave API no habilitados en la cuenta: escriba a soporte.
409payments_not_readyWAVA/PayPal no puede cobrar; reason dice por qué (payments_addon_off, kyc_pending, wava_sandbox_inactive…).
422mode_not_allowed / return_url_not_allowed / validation_failedModo no permitido / dominio de retorno no autorizado / datos inválidos (errors[]).
429checkouts_per_hour / invoices_per_day / cop_per_dayLímite alcanzado (Retry-After).

Enlaces de pago (Ver y pagar)

Un enlace de pago es la página pública https://<cuenta>.miplata.co/p/<token> donde el adquiriente ve el documento y paga: COP con Nequi, Daviplata o Bre-B (WAVA); USD, EUR, GBP, CAD, CHF, MXN o JPY con PayPal. Pagar no pide nada: quien tiene el enlace (como quien tiene el QR impreso) puede pagar. Los últimos 4 dígitos del NIT o la cédula del adquiriente (el celular si es consumidor final) solo desbloquean el detalle del documento —nombre, líneas, totales— y el PDF. Es el mismo enlace del QR del PDF y del correo.

Creación automática: al aceptarse una factura de venta (y al generar el PDF de una cotización abierta o de una factura aceptada sin enlace) miPlata crea el enlace por su cuenta e imprime su QR en el PDF, si Configuración → Plantillas y envío → «Incluir el código QR de pago en el PDF» está activo (predeterminado). Un cliente marcado en Clientes → «Sin enlace de pago» no recibe enlaces automáticos (sin QR ni botón en los correos; su PDF muestra los datos bancarios); POST /invoices/{id}/pay-link sigue emitiéndolo a propósito para ese documento.

POST /invoices/{id}/pay-link lo emite para una factura de venta emitida (01–04, no borrador) o una cotización abierta, por el saldo neto (total − anticipos − retenciones − pagos; en COP ≥ $ 2.000). Es idempotente (200 con el mismo url); reissue: true emite un token nuevo; return_url (dominio en Dominios de retorno) lleva al comprador de vuelta a su sitio tras pagar, sin cambiar el token. Si no se puede cobrar responde 409 paylink_not_eligible con reason (tabla abajo) y un mensaje que dice qué hacer.

Al pagarse, miPlata registra el pago y envía invoice.paid. Si era una cotización, la convierte en factura electrónica y la transmite: invoice.paid trae la factura nueva, quote_id y su order_reference. Pagar un enlace de una factura ya emitida solo registra el pago; nunca crea otro documento. Un pago que llegó por fuera (efectivo, transferencia directa) se registra con POST /invoices/{id}/payments (sección Pagos recibidos fuera de la pasarela).

Vence a los 90 días o con la cotización; DELETE /invoices/{id}/pay-link lo revoca (pedido cancelado, pagó por otro medio) y un enlace revocado o vencido no se puede pagar. Tras 20 intentos fallidos de desbloqueo se cierran el detalle y el PDF de ese enlace; pagar sigue siendo posible (así un tercero que adivina no le quita al comprador su forma de pago). Para reabrir el detalle emita otro con reissue. El estado expired no se guarda: GET lo calcula cuando un enlace pending pasó su expires_at. GET /invoices/{id}/pay-link responde al instante y sirve para consultar mientras el comprador paga.

  • POST/invoices/{id}/pay-linkEmite o devuelve el enlace (201 nuevo / 200 reutilizado).
  • GET/invoices/{id}/pay-linkEstado del enlace más reciente.
  • DELETE/invoices/{id}/pay-linkRevoca el enlace vigente.

Respuesta (objeto pay_link)

CampoTipoReq.Descripción
id / statusstringplx_…; pending | paid | locked | revoked | disputed, o expired (calculado: pending con expires_at vencido). Con locked o revoked emita otro con reissue.
urlstringPágina de pago; solo mientras está pending.
amount / currencyinteger / stringSaldo en unidades menores (COP: pesos).
rail / providerstringlive | sandbox; wava | paypal.
return_urlstringAdónde vuelve el comprador tras pagar, o null.
flagstringdouble_paid | amount_mismatch.
expires_at / paid_at / created_atISO 8601Vencimiento, pago, creación.

Motivos de 409 paylink_not_eligible (reason)

reasonQué significa / qué hacer
draftUn borrador no se puede cobrar: emítalo primero.
not_sales_invoiceSolo facturas de venta (01/02/03/04) y cotizaciones abiertas llevan enlace (no notas, soporte ni nómina).
already_paidEl documento ya está pagado.
no_unlock_digitsEl adquiriente necesita NIT/cédula (o celular si es consumidor final) con al menos 4 dígitos para abrir el detalle y el PDF.
currency_unsupportedLa moneda no tiene proveedor de cobro (COP → WAVA; USD/EUR/GBP/CAD/CHF/MXN/JPY → PayPal).
amount_too_lowEl saldo a cobrar es cero, negativo o menor de $ 2.000 COP.
vaultModo Bóveda: la plataforma no puede servir la página de pago.
flag_offLos enlaces de pago no están habilitados en esta cuenta (soporte de miPlata).
payments_addon_offSistema Pagos no está activo: Configuración → Pagos en línea.
wava_not_connectedWAVA no está conectado en el entorno actual (Sandbox / Producción).
wava_sandbox_inactiveLa tienda WAVA de pruebas aún no está activa (soporte de miPlata).
kyc_pendingWAVA aún verifica su empresa; mientras tanto use Sandbox.
wava_not_liveLos cobros WAVA en producción aún no están confirmados por miPlata.
paypal_offPayPal no está habilitado en esta cuenta.
paypal_not_configuredPayPal no está configurado (client id, secret y webhook id).
mint_failedError interno al emitir el enlace; reintente.

Entrega de invoice.paid (cotización pagada y convertida)

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/pay-link \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "reissue": false,
  "return_url": "https://tu-sitio.co/compra/gracias"
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/pay-link", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "reissue": false,
  "return_url": "https://tu-sitio.co/compra/gracias"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "reissue": false,
  "return_url": "https://tu-sitio.co/compra/gracias"
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/pay-link",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/pay-link");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "reissue" => false,
    "return_url" => "https://tu-sitio.co/compra/gracias"
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201 de POST /invoices/{id}/pay-link

{
  "id": "plx_7f95583cd5a7377eff16f4d7",
  "status": "pending",
  "url": "https://c901234567.miplata.co/p/eKx2VumBK3qLZ8WcL_6hSIvM6JoI_2MQQTBGiSH32pg",
  "amount": 116500,
  "currency": "COP",
  "rail": "live",
  "provider": "wava",
  "flag": null,
  "expires_at": "2027-01-04T15:00:04",
  "paid_at": null,
  "created_at": "2026-10-06T15:00:04",
  "return_url": "https://tu-sitio.co/compra/gracias"
}

Pagos recibidos fuera de la pasarela

Cuándo: dinero que ya tiene y que no pasó por miPlata — efectivo en caja, transferencia o consignación directa a su cuenta, datáfono propio. Registrarlo deja la factura pagada en Cartera, en GET /invoices/{id} (amount_paid y balance) y en el PDF, y dispara invoice.paid. No cobra, no emite otro documento, no modifica el XML ni el CUFE y no consume cupo.

Cuándo no: los pagos por enlace de pago o cobro de miPlata ya quedan registrados solos. Registrarlos otra vez responde 409 already_paid.

Camino 1 — la factura ya está emitida: POST /invoices/{id}/payments con amount, payment_method, reference y payment_date. Camino 2 — el dinero se confirmó antes de facturar (caja, matrícula): POST /invoices con un objeto payment; la factura nace pagada en una sola llamada (sin amount = saldo completo; con amount = pago parcial). En ambos, payment_means_code sigue siendo el medio fiscal del XML y lo fija usted como siempre (10 efectivo, 30/31 transferencia, 42 consignación, 48/49 tarjeta).

Reintentos sin duplicados: reference es única por factura. Repetir la misma reference devuelve el mismo recibo (200, Idempotent-Replayed: true); la misma reference con otro importe responde 409 reference_conflict. Puede añadir Idempotency-Key, pero la reference ya protege.

Si POST /invoices con payment responde 201 con receipt_error: la factura existe y está aceptada; solo falló el recibo. Repita POST /invoices/{id}/payments con la misma reference.

Permisos y Sandbox: cualquier llave o PAT con alcance a la empresa, sin configuración adicional; en Sandbox funciona igual (sobre documentos de habilitación). Un documento de la otra empresa o del otro entorno responde 404.

  • POST/invoices/{id}/paymentsRegistra un pago recibido sobre una factura emitida (201; 200 si la reference ya existía).
  • GET/invoices/{id}/paymentsRecibos del documento (app, API y pasarela) con total, retenciones, pagado y saldo.
  • GET/payments/receiptsTodos los recibos de la empresa en el entorno activo (date_from, date_to, payment_method; company_id para otra empresa; paginado).

Cuerpo de POST /invoices/{id}/payments (y objeto payment de POST /invoices)

CampoTipoReq.Descripción
amountstringImporte recibido, decimal con máximo dos decimales, ≤ balance. En payment de POST /invoices es opcional: omitido = saldo completo.
payment_methodstringcash | transfer | card | other. Predeterminado cash.
referencestringObligatoria, 1–100 caracteres, única por factura: número de consignación, recibo de caja, comprobante del datáfono. Es la protección contra duplicados.
payment_datestringYYYY-MM-DD de la recepción real, no futura. Omitida = hoy.
notesstringOpcional, máximo 500 caracteres.

Errores

CódigoQué pasóQué hacer
409 overpaymentamount supera el saldo (el detalle trae balance).Registre como máximo el balance.
409 already_paidLa factura ya se pagó por enlace o cobro de miPlata.No registre nada; consulte GET /invoices/{id}/payments.
409 not_payableCotización, nota, documento soporte, borrador o documento rechazado (reason lo indica).Emita primero la factura de venta.
409 reference_conflictLa reference ya existe en esta factura con otro importe.Use otra reference o repita el mismo importe.
422 payment_invalid / amount_invalid / payment_date_invalidCampo mal formado (en POST /invoices con payment no se emite nada).Corrija el campo y reintente.
404Documento de otra empresa o del otro entorno.Verifique id, llave y entorno (Configuración → Entorno).

Qué consultar después

NecesitaUse
Saldo de una facturaGET /invoices/{id} → balance (total − anticipo − retenciones − pagado)
Recibos de una facturaGET /invoices/{id}/payments → items con source app | api | gateway
Todos los recibos del mes (resumen contable)GET /payments/receipts?date_from=2026-10-01&date_to=2026-10-31 (X-Total-Count; company_id para la otra empresa)
Aviso en su sistemaWebhook invoice.paid con payment.provider = manual y payment.provider_ref = su reference

Camino 2 — POST /invoices con payment (nace pagada)

curl -sS -X POST https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/payments \
  -H "X-API-Key: TU_LLAVE" \
  -H "Content-Type: application/json" \
  -d '{
  "amount": "119000",
  "payment_method": "transfer",
  "reference": "CONSIG-2026-10-09-0471",
  "payment_date": "2026-10-09",
  "notes": "Transferencia Bancolombia, clases octubre"
}'
const res = await fetch("https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/payments", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.MIPLATA_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "amount": "119000",
  "payment_method": "transfer",
  "reference": "CONSIG-2026-10-09-0471",
  "payment_date": "2026-10-09",
  "notes": "Transferencia Bancolombia, clases octubre"
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
console.log(data);
import json, os, urllib.request

payload = json.dumps({
  "amount": "119000",
  "payment_method": "transfer",
  "reference": "CONSIG-2026-10-09-0471",
  "payment_date": "2026-10-09",
  "notes": "Transferencia Bancolombia, clases octubre"
}).encode()

req = urllib.request.Request(
    "https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/payments",
    data=payload,
    method="POST",
    headers={
        "X-API-Key": os.environ["MIPLATA_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(req) as resp:
    print(json.load(resp))
<?php
$ch = curl_init("https://TU-EMPRESA.miplata.co/api/external/invoices/{id}/payments");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "X-API-Key: " . getenv("MIPLATA_API_KEY"),
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    "amount" => "119000",
    "payment_method" => "transfer",
    "reference" => "CONSIG-2026-10-09-0471",
    "payment_date" => "2026-10-09",
    "notes" => "Transferencia Bancolombia, clases octubre"
]));
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Respuesta 201 de POST /invoices/{id}/payments

{
  "id": "0f3c7a2e-5b19-4d8e-9c41-2a7e6f1d3b05",
  "invoice_id": "28349fb9-043f-4673-8934-dfab34bac9bc",
  "amount": "119000.00",
  "currency": "COP",
  "payment_date": "2026-10-09",
  "payment_method": "transfer",
  "reference": "CONSIG-2026-10-09-0471",
  "notes": "Transferencia Bancolombia, clases octubre",
  "source": "api",
  "created_at": "2026-10-09T20:14:07"
}

Datos del comprador (DIAN)

miPlata normaliza al comprador antes de facturar: calcula o verifica el dígito de verificación del NIT, resuelve la ciudad contra la tabla DIVIPOLA completa (por código DANE, o por nombre dentro del departamento indicado —acepta el código CO-XX de WooCommerce— y, si no, por nombre único en el país), asigna el régimen (49 persona natural, 48 persona jurídica) y las responsabilidades fiscales (R-99-PN / O-13) cuando no vienen, y toma el teléfono en formato +57.

Si falta el documento, el nombre o el tipo, la política missing_data de la conexión decide: consumidor_final factura al NIT genérico 222222222222 (y lo advierte en warnings), ask_buyer crea el cobro en held y entrega complete_url —una página de miPlata, válida 72 horas, donde el comprador completa sus datos—, y hold lo deja en held hasta que la tienda reenvíe los datos. POST /payments/buyer/validate aplica exactamente las mismas reglas y devuelve errors, warnings y el comprador normalizado sin crear nada.

Tipos de documento aceptados

doc_typeDocumento
13Cédula de ciudadanía
31NIT (el DV se calcula si no viene; si viene, se verifica)
22Cédula de extranjería
41Pasaporte
42Documento de identificación extranjero
47PEP
48PPT
50NIT de otro país
11Registro civil
12Tarjeta de identidad
21Tarjeta de extranjería
91NUIP

Códigos de error de validación

codeCampoSignificado
buyer_incompletebuyerFalta documento / nombre y la política no es consumidor_final.
nit_invalid / nit_dv_mismatchbuyer.doc_numberNIT mal formado / dígito de verificación incorrecto (se indica el esperado).
email_requiredbuyer.emailSin correo y la política exige enviarla.
city_unresolvedbuyer.cityAdvertencia: ciudad no reconocida; se factura sin código de municipio.
currency_unsupported / currency_provider_mismatch / currency_not_enabledcurrencyMoneda no soportada / WAVA no cobra en moneda extranjera / la conexión no permite moneda extranjera.
amount_mismatch / amount_over_limitamountEl total declarado no coincide / supera el máximo por cobro.
lines_too_many / total_not_positivelinesMás líneas que el límite / total ≤ 0.
mode_not_allowed / identity_lockedmode / project_idLa conexión no permite ese modo / la identidad de facturación se fija en miPlata, no en la tienda.

Conexiones de tienda

Las rutas /api/connect/* van firmadas con X-MP-* (Ed25519): firma X-MP, no X-API-Key. No aceptan la llave API, la llave API no las abre y están fuera de la URL base /api/external.

Una conexión de tienda es una credencial distinta de la llave API, pensada para plugins que corren en el servidor de la tienda: alcance fijo y mínimo (cobros y documentos de sus propios pedidos; nunca listados, clientes, nómina, llaves ni webhooks), límites ajustables y una identidad de facturación por conexión (proyecto o logo). Se crea desde su instancia → Configuración → Conexiones de tienda (solo administradores), que genera un código de conexión MPC-<cuenta>-XXXX-XXXX-XXXX: de un solo uso, válido 10 minutos, invalidado tras 5 intentos fallidos. miPlata nunca le pedirá ese código.

El plugin canjea el código con POST /api/connect/redeem enviando su clave pública Ed25519, la versión del plugin, el dominio de la tienda y una prueba de posesión (firma sobre redeem\n<sha256 del código>\n<dominio>). miPlata verifica además la propiedad del dominio leyendo la clave publicada por el plugin en /.well-known/miplata-connect.json (o /?rest_route=/miplata/v1/connect-key, /wp-json/miplata/v1/connect-key): una sola redirección al mismo dominio, 5 segundos, agente miPlata-Connect/1. Al aprobarse, todos los administradores reciben un correo con la huella de la clave y un enlace «No fui yo» que revoca la conexión.

Después, cada solicitud va firmada: encabezados X-MP-Connection (id), X-MP-Timestamp (segundos Unix, ±300 s; use X-MP-Server-Time de cualquier respuesta para corregir el reloj), X-MP-Nonce (aleatorio, de un solo uso durante 10 minutos), X-MP-Signature (Ed25519 en base64url sobre la cadena canónica) y X-MP-Plugin (versión; una versión inferior a la mínima recibe 426). La cadena canónica es v1\n{MÉTODO}\n{host en minúsculas}\n{ruta}\n{query ordenada}\n{timestamp}\n{nonce}\n{sha256 hex del cuerpo}; la query se ordena por clave y valor con codificación RFC 3986. Los vectores de prueba oficiales (clave fija, cinco solicitudes y sus firmas) están en el repositorio de los plugins para validar su implementación byte a byte.

Las conexiones admiten además mode invoice: la tienda ya cobró (indique gateway y paid). Un pedido pagado con la app oficial de WAVA solo se factura cuando WAVA lo confirma a miPlata (misma referencia, o monto + moneda + correo en 30 minutos); mientras tanto queda unverified, a las 24 h se marca para revisión y un administrador puede confirmarlo. Si faltan datos del comprador, la política missing_data decide (held + complete_url con ask_buyer).

El servidor guarda solo claves públicas. Rote la clave cada 90 días con POST /api/connect/rotate (la anterior sigue válida 10 minutos), notifique problemas de entrega con POST /api/connect/delivery-failed, envíe un diagnóstico diario con POST /api/connect/health y revoque al desinstalar con DELETE /api/connect/self. Veinte firmas inválidas en diez minutos suspenden la conexión quince minutos; un administrador puede suspenderla, rotarla o revocarla en cualquier momento y leer su auditoría de 90 días.

Los webhooks hacia la tienda (checkout.paid, checkout.invoiced, checkout.credit_note) llegan al dominio vinculado, firmados con la clave Ed25519 de la cuenta (webhook_public_key en GET /payments/capabilities) sobre v1\n{X-MP-Timestamp}\n{sha256 hex del cuerpo}, con un cuerpo mínimo (ids y estado, nunca datos del comprador). Verifique la firma y vuelva a consultar el cobro antes de actuar. Se reintentan cinco veces; tres rechazos 403/406/429 seguidos marcan la conexión como bloqueada por el firewall de la tienda.

  • POST/api/connect/redeemCanjea el código de conexión (sin X-API-Key ni firma X-MP previa; prueba de posesión + propiedad del dominio).
  • POST/api/connect/rotateFirma X-MP, no X-API-Key. Nueva clave pública; la anterior vale 10 minutos más.
  • POST/api/connect/healthFirma X-MP, no X-API-Key. Diagnóstico diario (versión, WAVA, SMTP, cron). Sin datos personales.
  • POST/api/connect/delivery-failedFirma X-MP, no X-API-Key. La tienda no pudo enviar la factura (sin correo, SMTP).
  • DELETE/api/connect/selfFirma X-MP, no X-API-Key. Revoca la conexión (desinstalación).

Límites por conexión (valor por defecto / techo del operador)

LímitePredeterminadoTecho
Solicitudes por minuto60600
Cobros por hora3003.000
Monto máximo por cobro (COP)50.000.000500.000.000
Líneas por cobro100500
Facturas por día1005.000
COP facturados por día20.000.0002.000.000.000

Códigos de respuesta propios de las conexiones

HTTPcodeSignificado
401signature_missing / signature_invalid / replay / ts_skewFaltan encabezados / firma incorrecta / nonce repetido / reloj desfasado (use X-MP-Server-Time).
401connection_unknownConexión inexistente, pendiente o revocada (indistinguibles a propósito).
403scope_missing / connection_suspended / ip_not_allowedRuta fuera del alcance / suspendida (Retry-After) / IP no permitida.
404not_foundEl cobro no existe o pertenece a otra conexión.
409not_ready_to_invoiceRango agotado, certificado vencido o cuenta no lista (health).
423vault_lockedCuenta en Modo Bóveda sin clave de ingreso.
426plugin_outdatedActualice el plugin (min_version).
429rate_limited / invoices_per_day / cop_per_dayLímite alcanzado (Retry-After).
503store_connections_off / maintenanceConexiones desactivadas temporalmente / ventana de mantenimiento (Retry-After).

Cadena canónica firmada (una línea por campo; la query vacía es una línea vacía)

v1
POST
tu-empresa.miplata.co
/api/external/payments/checkouts

1760000123
bm9uY2UtMTIzNDU2Nzg5MDEy
16f18021a964da88045af235dade647c52ee3b5a3cad95020a314da56fc90610

Solicitud firmada

POST /api/external/payments/checkouts HTTP/1.1
Host: tu-empresa.miplata.co
Content-Type: application/json
X-MP-Connection: conn_7f3a9c1e2b4d
X-MP-Timestamp: 1760000123
X-MP-Nonce: bm9uY2UtMTIzNDU2Nzg5MDEy
X-MP-Signature: WZJB-WZvOLHKhpr18CkKE4-mbmgxtNOgvBgoqmkK3yByHjtryO7a7e4C2-2JByQNq0sH4mUT9GPYs8beyDqLDg
X-MP-Plugin: 1.0.0
Idempotency-Key: wc-1001

{"mode":"invoice","order_reference":"1001","amount":"119000.00","currency":"COP"}

Plugins oficiales y «Construya su plugin»

miPlata Facturación para WooCommerce (GPL, para WordPress ≥ 6.2, PHP ≥ 7.4 con sodium) factura cada pedido pagado con los campos nativos de WooCommerce: líneas con cupones aplicados, tasas de impuesto por clase (Estándar → IVA 19, Reducida → IVA 5, Cero → exento; INC, excluido y bolsas configurables), envío y cargos como líneas, productos virtuales como servicio, empresa + NIT como persona jurídica y el estado CO-XX como departamento DANE. Solo añade tipo y número de documento, y únicamente si ningún otro plugin ya lo pide. Envía la factura (PDF + XML firmado) con el correo de la propia tienda y muestra «Ver factura» en Mi cuenta y en la página del pedido; un reembolso genera la nota crédito. Las pasarelas «Pagar con WAVA (miPlata)» y «Pagar con PayPal (miPlata)» son opcionales y se ocultan cuando el plugin oficial de WAVA está activo.

La app de Tiendanube (solo facturación; los pagos siguen en la app de WAVA o la pasarela de la tienda) se instala desde tiendanube.miplata.co, se conecta con el código generado para el ID de la tienda, factura los pedidos pagados con los datos nativos del pedido, muestra «Ver factura» en la página del pedido (NubeSDK) y envía la factura por correo de miPlata, porque los correos de Tiendanube no admiten adjuntos de apps.

Para construir su propio plugin o integración: elija «Otra tienda / API propia» en Integraciones y siga el protocolo de la sección Conexiones de tienda (o, para su propio sitio o ERP, una llave API con los endpoints de Cobros: modos pay y pay_and_invoice). Publique su clave en /.well-known/miplata-connect.json, firme cada solicitud, cachee GET /payments/capabilities diez minutos, cree exactamente un cobro por pedido con Idempotency-Key, reciba los webhooks firmados y, como respaldo, consulte el estado una vez a los diez minutos y luego cada hora. El código fuente de ambos plugins y los vectores de prueba de firma, totales y comprador están disponibles como referencia de implementación.

  • WooCommerce: directorio oficial de WordPress.org (wordpress.org/plugins/miplata, «miPlata FACTURAR»), con actualizaciones desde el panel de WordPress; guía en miplata.co/blog/facturacion-electronica-woocommerce.
  • Tiendanube: tiendanube.miplata.co (instalación OAuth) y la tienda de aplicaciones de Tiendanube tras la homologación.
  • Modo prueba: en cuentas en ambiente de pruebas DIAN, una conexión puede usar una página de pago simulada (sin cobro real) para probar el flujo completo, incluida la emisión en el ambiente de habilitación.
  • Instalación asistida o integración hecha por miPlata (tienda, POS, ERP o sistemas legacy), con precio fijo: facturar.miplata.co/integraciones.

Asistentes IA

Conectar ChatGPT y Claude (conector MCP) — requiere activación por miPlata

El conector oficial mcp.miplata.co permite que un asistente de IA (ChatGPT o Claude) consulte su cuenta y, si usted lo autoriza, emita documentos en su nombre, con el protocolo MCP (Model Context Protocol) sobre HTTP y OAuth 2.1. No usa la llave API: cada asistente recibe su propia conexión, con alcance fijo y revocable, creada desde su instancia → Configuración → Conexiones → «Conectar un asistente de IA» (solo administradores). Esta función está disponible por activación de miPlata (plan con miPlata IA) y no aparece en Conexiones hasta entonces.

Flujo: elija ChatGPT o Claude y el alcance (Solo lectura o Lectura y emisión), copie el código de conexión MPC-… (de un solo uso, válido 10 minutos) y, en el asistente, agregue el conector con la URL https://mcp.miplata.co/mcp. El asistente lo lleva a una página de miPlata donde pega el código; desde ese momento el asistente firma cada llamada con una credencial Ed25519 propia (encabezados X-MP-*, los mismos de las conexiones de tienda) y miPlata registra cada solicitud en la auditoría de la conexión.

Alcance Solo lectura (assistant_read): GET /company, /invoices, /invoices/{id}, /invoices/{id}/pdf, /invoices/{id}/pay-link, /customers, /usage y /ranges. Lectura y emisión (assistant_write) añade POST /invoices, POST /invoices/{id}/pay-link, POST /invoices/{id}/retry-now y POST /invoices/{id}/payments; requiere además el flag mcp_writes y confirma cada emisión con un resumen del documento que usted aprueba en el chat. Un asistente nunca abre cobros, nunca lee nómina, llaves, webhooks ni configuración, y recibe los mismos 402 / 403 / 409 / 422 que la API.

Herramientas del conector: search y fetch (documentación y catálogo de errores DIAN), explain_dian_error, api_reference, get_company, list_ranges, list_invoices, get_invoice, get_invoice_history_explained, get_invoice_pdf, list_customers, get_usage, get_pay_link y, con emisión, create_invoice, retry_invoice y create_pay_link. Las respuestas que el asistente recibe pasan por un filtro que elimina instrucciones incrustadas en notas o nombres de clientes.

Máximo tres conexiones de asistente por cuenta (ampliable por miPlata hasta diez). Rote, suspenda o revoque una conexión en cualquier momento desde Conexiones; revocar invalida el conector al instante en el asistente. Los datos viajan al proveedor del asistente (OpenAI o Anthropic) bajo sus términos: miPlata no entrena modelos con su información y solo entrega al asistente lo que la herramienta devuelve.

  • ChatGPT: en la configuración del asistente, Conectores, cree uno con la URL https://mcp.miplata.co/mcp y autorice con el código.
  • Claude: en la configuración del asistente, Conectores, añada un conector personalizado con la misma URL y autorice con el código.
  • Ambiente de pruebas: una cuenta en habilitación DIAN emite documentos de prueba también desde el asistente; nada llega a producción.
  • GET/.well-known/oauth-authorization-serverEn mcp.miplata.co. Descubrimiento OAuth 2.1 (PKCE obligatorio).
  • POST/mcpEn mcp.miplata.co. Endpoint MCP (Streamable HTTP) con token Bearer del conector.

Alcances de asistente

PresetScopesHerramientas
assistant_readread:company, read:invoices, read:customers, read:usage, read:ranges, read:paylinksConsultas, PDF, enlace de pago existente, explicación de rechazos
assistant_writelos anteriores + write:invoices, write:paylinks+ create_invoice, retry_invoice, create_pay_link (confirmación en el chat; flag mcp_writes)

Asistente de tienda (Lucas Tienda) — canal API

Lucas Tienda es el asistente público de su tienda: responde a sus clientes sobre productos, envíos, pagos, horarios y políticas usando solo las fuentes que usted aprueba, y puede proponer un carrito a partir de su catálogo público (nunca cobra ni reserva existencias). Es un complemento prepagado (Configuración → Complementos → Lucas Tienda) que se configura en la pestaña Lucas Tienda de su instancia: fuentes, perfil, canales, pruebas, métricas y consentimientos.

Canales incluidos: la tienda /tienda (burbuja activada con un interruptor), el widget para cualquier sitio (<script src="https://bot.miplata.co/w/<id-del-bot>.js"> con la lista de orígenes permitidos), el plugin de WooCommerce (interruptor «Asistente miPlata» en sus ajustes) y el canal API descrito aquí, para apps o sitios propios. El asistente corre en una infraestructura separada de su cuenta: no tiene acceso a facturas, clientes, nómina ni configuración, solo al contenido aprobado y al catálogo público.

Canal API: POST https://bot.miplata.co/v1/<id-del-bot>/answer con el encabezado X-Bot-Key (llave del canal API, generada en Canales; revocable) y un cuerpo JSON {session_id?, message, page_url?}. La respuesta trae kind (answer, search, cart o handoff), el texto en Markdown reducido (negrita, listas y enlaces solo a su tienda), las tarjetas de productos o el carrito propuesto (ids y precios del catálogo, con un enlace /tienda?cart=… para continuar la compra) y answer_id para POST /v1/<id-del-bot>/feedback {session_id, answer_id, vote}. Los mensajes van limitados a 500 caracteres y 12 por sesión; una sesión caduca a los 30 minutos.

Límites: respuestas por día y por mes según el plan (402 quota_exhausted al agotarse; compre un paquete adicional), 30 mensajes por minuto por bot y 10 por minuto por dirección IP (429 con Retry-After). Si el período prepagado vence o miPlata pausa el asistente, el canal responde 404 y el widget se oculta. Las conversaciones se conservan 30 días; los contadores de uso, 24 meses.

  • POST/v1/{bot}/answerEn bot.miplata.co, X-Bot-Key. Una pregunta del cliente → respuesta estructurada.
  • POST/v1/{bot}/feedbackEn bot.miplata.co, X-Bot-Key. Pulgar arriba/abajo sobre una respuesta.

Códigos del canal API

HTTPcodeSignificado
401forbiddenX-Bot-Key ausente, revocada o de otro bot.
403originOrigen no permitido (widget).
404not_foundBot inexistente, pausado o con período vencido (indistinguibles a propósito).
402quota_exhaustedRespuestas del día o del mes agotadas.
409session_fullLa sesión llegó a 12 mensajes; abra otra.
429rate_limitedLímite por minuto (Retry-After).
503model_unavailableEl modelo no respondió a tiempo; reintente.

Pregunta por el canal API

curl -sS -X POST https://bot.miplata.co/v1/b_XXXXXXXXXXXXXXXX/answer \
  -H "X-Bot-Key: TU_LLAVE_DE_BOT" \
  -H "Content-Type: application/json" \
  -d '{"message":"¿Tienen mochilas wayuu grandes y cuánto demora el envío a Cali?"}'

Respuesta

{
  "kind": "cart",
  "session_id": "s_8f1c…",
  "answer_id": "a_5c2e…",
  "text": "Sí: la **Mochila wayuu grande** (120.000 COP). El envío a Cali tarda 2 a 4 días hábiles.",
  "cart": {
    "lines": [{ "id": "p_42", "name": "Mochila wayuu grande", "qty": 1, "unit_gross": 120000 }],
    "total_gross": 120000,
    "currency": "COP",
    "checkout_url": "https://tu-empresa.miplata.co/tienda?cart=…"
  },
  "handoff": { "whatsapp": "+573001234567" },
  "assistant_notice": "Asistente automático. Puede equivocarse."
}