MailMerge API 789.mx
Referencia OpenAPI
Documentación de la API

Conecta tu aplicación a MailMerge#

MailMerge convierte una plantilla de Microsoft Word en un documento terminado —DOCX o PDF— a partir de los datos que envíe tu aplicación en formato JSON. Todo se hace por HTTP: subes la plantilla una vez y después generas tantos documentos como necesites.

PARA QUÉ SIRVE

Contratos, pagarés, cotizaciones

Cualquier documento que hoy armas a mano en Word y necesitas producir en serie desde tu sistema.

QUÉ ENVÍAS

Un JSON con tus datos

Campos sueltos, filas de tabla, imágenes por URL y hasta fragmentos de HTML con formato.

QUÉ RECIBES

El archivo, listo

PDF para distribuir o DOCX editable, descargable con una petición autenticada.

Cómo funciona#

El modelo es sencillo: una plantilla es un archivo .docx con campos combinables incrustados; un documento es el resultado de combinar esa plantilla con un conjunto de datos. Las plantillas se suben una sola vez y se reutilizan; los documentos se generan bajo demanda.

1 · LOGIN 2 · PLANTILLA 3 · GENERAR 4 · DESCARGAR Obtienes un token Subes el .docx Envías tus datos Recibes el PDF o DOCX Y renuevas al caducar Una vez por plantilla Una vez por documento Disponible 7 días
  1. Autentícate

    Envía tu correo y contraseña a /api/v2/auth/login y guarda el access_token. Ese token acompaña a todas las demás llamadas.

  2. Sube tu plantilla

    Envía el .docx a /api/v2/templates. La API detecta automáticamente los campos combinables y te devuelve la lista en merge_fields, junto con el id de la plantilla.

  3. Genera el documento

    Llama a /api/v2/documents con el template_id, tu referencia interna (ref_id) y los datos. Eliges si quieres pdf o docx.

  4. Descarga el resultado

    La respuesta trae el id del documento y su download_url. Descárgalo con /api/v2/documents/{id}/download.

✓

La generación es síncrona. Cuando POST /api/v2/documents responde 201, el archivo ya está listo: puedes descargarlo de inmediato. El endpoint de estado existe para quienes prefieran confirmar por polling, pero no es necesario en el flujo normal.

Antes de empezar#

Necesitas dos cosas:

i

Nombra los campos como en tus datos. El nombre del campo combinable en Word debe coincidir, carácter por carácter, con la clave que envías en merge_data.fields. Es sensible a mayúsculas, acentos y guiones bajos. Si no estás seguro de cómo quedaron los nombres, súbela y mira merge_fields en la respuesta.

URLs base y entornos#

Todo el servicio vive en https://mailmerge.789.com.mx. Desarrolla contra el entorno de pruebas y cambia a producción cuando tu integración esté lista: los endpoints son idénticos, solo cambia la URL base.

EntornoURL basePara qué
Producción https://mailmerge.789.com.mx/api/v2 La API actual. Documentada en esta página.
Pruebas https://mailmerge.789.com.mx/api/v2/test Mismos endpoints y datos de prueba. Úsalo mientras desarrollas.

Además hay tres recursos útiles, sin autenticación:

RecursoURL
Swagger UI interactivo/docs
Especificación OpenAPI 3.1/openapi.json
Estado del servicio/health
i

Los ejemplos de esta página usan $MM (o MM) como URL base. Defínela una vez y apunta a pruebas mientras desarrollas:

export MM=https://mailmerge.789.com.mx/api/v2/test

Guía rápida en 4 llamadas#

Este es el recorrido completo, de cero a PDF descargado. Cambia el selector de lenguaje en la barra del ejemplo para verlo en cURL, PHP, JavaScript o Python; la selección se aplica a toda la página.

De cero a PDF
# URL base — usa /api/v2/test mientras desarrollas
MM=https://mailmerge.789.com.mx/api/v2
# El token de tu aplicacion acompana a TODAS las llamadas, login incluido
APP_TOKEN=mmapp_a1b2c3d4_kZ9rTq2WmNp4XeLs7VhB

# 1 · Autenticarte y guardar el token
TOKEN=$(curl -s -X POST "$MM/auth/login" \
  -H "X-App-Token: $APP_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"email":"tu-usuario@empresa.mx","password":"TuContrasena123"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')

# 2 · Subir la plantilla (una sola vez por plantilla)
curl -s -X POST "$MM/templates" \
  -H "Authorization: Bearer $TOKEN" \
  -F 'file=@contrato.docx' \
  -F 'ref_id=CONTRATO-BASE' \
  -F 'name=Contrato de arrendamiento' \
  -F 'template_type=contract'
# → {"id": 550, "merge_fields": ["nombre_cliente","fecha_contrato","monto_total"], ...}

# 3 · Generar el documento con tus datos
curl -s -X POST "$MM/documents" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "template_id": 550,
    "ref_id": "CONTRATO-2026-0187",
    "merge_data": {
      "fields": {
        "nombre_cliente": "Distribuidora Alamo S.A. de C.V.",
        "fecha_contrato": "18/09/2026",
        "monto_total": "$128,400.00"
      }
    },
    "output_format": "pdf"
  }'
# → {"id": 1234, "status": "completed", "download_url": "/api/v2/documents/1234/download", ...}

# 4 · Descargar el archivo generado
curl -s "$MM/documents/1234/download" \
  -H "Authorization: Bearer $TOKEN" \
  -o contrato-2026-0187.pdf
<?php
// composer require guzzlehttp/guzzle
use GuzzleHttp\Client;

$MM = getenv('MAILMERGE_API') ?: 'https://mailmerge.789.com.mx/api/v2';
$http = new Client(['base_uri' => $MM . '/', 'http_errors' => false]);

// 1 · Autenticarte
$res = $http->post('auth/login', ['json' => [
    'email'    => 'tu-usuario@empresa.mx',
    'password' => 'TuContrasena123',
]]);
$auth  = json_decode((string) $res->getBody(), true);
$token = $auth['access_token'];
$auth_header = ['Authorization' => 'Bearer ' . $token];

// 2 · Subir la plantilla (una sola vez por plantilla)
$res = $http->post('templates', [
    'headers'   => $auth_header,
    'multipart' => [
        ['name' => 'file',          'contents' => fopen('contrato.docx', 'r')],
        ['name' => 'ref_id',        'contents' => 'CONTRATO-BASE'],
        ['name' => 'name',          'contents' => 'Contrato de arrendamiento'],
        ['name' => 'template_type', 'contents' => 'contract'],
    ],
]);
$plantilla = json_decode((string) $res->getBody(), true);
// $plantilla['merge_fields'] trae los campos detectados en el .docx

// 3 · Generar el documento
$res = $http->post('documents', [
    'headers' => $auth_header,
    'json'    => [
        'template_id' => $plantilla['id'],
        'ref_id'      => 'CONTRATO-2026-0187',
        'merge_data'  => [
            'fields' => [
                'nombre_cliente' => 'Distribuidora Alamo S.A. de C.V.',
                'fecha_contrato' => '18/09/2026',
                'monto_total'    => '$128,400.00',
            ],
        ],
        'output_format' => 'pdf',
    ],
]);
$documento = json_decode((string) $res->getBody(), true);

// 4 · Descargar el archivo
$http->get("documents/{$documento['id']}/download", [
    'headers' => $auth_header,
    'sink'    => 'contrato-2026-0187.pdf',
]);
// Node.js 18+ o cualquier navegador moderno (fetch nativo)
import { readFile, writeFile } from 'node:fs/promises';

const MM = process.env.MAILMERGE_API ?? 'https://mailmerge.789.com.mx/api/v2';

// 1 · Autenticarte
const login = await fetch(`${MM}/auth/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'tu-usuario@empresa.mx',
    password: 'TuContrasena123',
  }),
});
const { access_token } = await login.json();
const auth = { Authorization: `Bearer ${access_token}` };

// 2 · Subir la plantilla (una sola vez por plantilla)
const form = new FormData();
form.append('file', new Blob([await readFile('contrato.docx')]), 'contrato.docx');
form.append('ref_id', 'CONTRATO-BASE');
form.append('name', 'Contrato de arrendamiento');
form.append('template_type', 'contract');

const plantilla = await fetch(`${MM}/templates`, {
  method: 'POST', headers: auth, body: form,
}).then((r) => r.json());
// plantilla.merge_fields trae los campos detectados en el .docx

// 3 · Generar el documento
const documento = await fetch(`${MM}/documents`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    template_id: plantilla.id,
    ref_id: 'CONTRATO-2026-0187',
    merge_data: {
      fields: {
        nombre_cliente: 'Distribuidora Alamo S.A. de C.V.',
        fecha_contrato: '18/09/2026',
        monto_total: '$128,400.00',
      },
    },
    output_format: 'pdf',
  }),
}).then((r) => r.json());

// 4 · Descargar el archivo
const pdf = await fetch(`${MM}/documents/${documento.id}/download`, { headers: auth });
await writeFile('contrato-2026-0187.pdf', Buffer.from(await pdf.arrayBuffer()));
# pip install requests
import os
import requests

MM = os.getenv("MAILMERGE_API", "https://mailmerge.789.com.mx/api/v2")

# 1 · Autenticarte
login = requests.post(f"{MM}/auth/login", json={
    "email": "tu-usuario@empresa.mx",
    "password": "TuContrasena123",
}, timeout=30)
login.raise_for_status()
token = login.json()["access_token"]
auth = {"Authorization": f"Bearer {token}"}

# 2 · Subir la plantilla (una sola vez por plantilla)
with open("contrato.docx", "rb") as f:
    plantilla = requests.post(
        f"{MM}/templates",
        headers=auth,
        files={"file": ("contrato.docx", f)},
        data={
            "ref_id": "CONTRATO-BASE",
            "name": "Contrato de arrendamiento",
            "template_type": "contract",
        },
        timeout=60,
    ).json()
# plantilla["merge_fields"] trae los campos detectados en el .docx

# 3 · Generar el documento
documento = requests.post(f"{MM}/documents", headers=auth, json={
    "template_id": plantilla["id"],
    "ref_id": "CONTRATO-2026-0187",
    "merge_data": {
        "fields": {
            "nombre_cliente": "Distribuidora Alamo S.A. de C.V.",
            "fecha_contrato": "18/09/2026",
            "monto_total": "$128,400.00",
        },
    },
    "output_format": "pdf",
}, timeout=120).json()

# 4 · Descargar el archivo
pdf = requests.get(f"{MM}/documents/{documento['id']}/download",
                   headers=auth, timeout=120)
with open("contrato-2026-0187.pdf", "wb") as f:
    f.write(pdf.content)
!

Sube la plantilla una sola vez. El id que recibes en el paso 2 es permanente: guárdalo en tu base de datos y reutilízalo en cada generación. Volver a subir el mismo ref_id con el mismo template_type devuelve 409 Conflict.


Autenticación#

MailMerge usa JWT de tipo Bearer. Según cómo llegues, empieza en un punto distinto:

Usar un token que ya tienes#

Todas las llamadas protegidas piden lo mismo: el access_token en el encabezado Authorization, con el prefijo Bearer y un espacio. No hay nada que registrar ni configurar antes; con ese encabezado ya puedes llamar a cualquier endpoint.

Encabezado requerido en cada petición
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Un token, una empresa

Ese token no solo dice quién eres: determina la empresa en cuyo nombre actúas. MailMerge deduce la empresa a partir del usuario dueño del token y filtra todo por ella —las plantillas que listas, los documentos que generas, el directorio donde se guardan—. No existe ningún encabezado ni parámetro para cambiar de empresa dentro de una petición: la empresa es la del token.

Lo que eso significa al integrar:

Conviene configurarlo una sola vez en tu cliente HTTP, en lugar de repetirlo en cada llamada:

Configurar el token una vez
# Guarda el token en una variable de entorno y reutilizala
export MM=https://mailmerge.789.com.mx/api/v2
export TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

curl "$MM/templates" -H "Authorization: Bearer $TOKEN"
use GuzzleHttp\Client;

// El encabezado queda fijo en el cliente: todas las llamadas lo llevan
$http = new Client([
    'base_uri' => 'https://mailmerge.789.com.mx/api/v2/',
    'headers'  => ['Authorization' => 'Bearer ' . getenv('MAILMERGE_TOKEN')],
]);

$res = $http->get('templates');
const MM = 'https://mailmerge.789.com.mx/api/v2';
const auth = { Authorization: `Bearer ${process.env.MAILMERGE_TOKEN}` };

// Reutiliza `auth` en cada llamada
const plantillas = await fetch(`${MM}/templates`, { headers: auth })
  .then((r) => r.json());
import os
import requests

MM = "https://mailmerge.789.com.mx/api/v2"

# Una sesion con el encabezado ya puesto: todas las llamadas lo heredan
api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['MAILMERGE_TOKEN']}"

plantillas = api.get(f"{MM}/templates", timeout=30).json()

Comprueba que tu token sirve

Antes de escribir nada más, haz esta llamada: es la más barata de la API y te dice de inmediato si el token es válido.

Prueba de humo
curl -i "$MM/templates" -H "Authorization: Bearer $TOKEN"
Lo que vesQué significa
200 con un arreglo [] o con plantillasEl token sirve. Ya puedes integrar.
401 {"detail": "Missing or invalid authorization header"}Falta el encabezado, o le falta el prefijo Bearer  con su espacio.
401 {"detail": "Could not validate credentials"}El token está mal copiado o ya caducó. Renuévalo con /auth/refresh.
!

Los dos tropiezos más comunes al copiar un token: olvidar la palabra Bearer y el espacio que la sigue —el valor del encabezado no es el token a secas—, y arrastrar un salto de línea al pegarlo. Un JWT es una sola línea larga, sin espacios.

TokenVigenciaPara qué sirve
access_token8 horas en pruebas · 30 días en producciónAutoriza cada llamada a la API.
refresh_token7 días en pruebas · 182 días en producciónObtiene un access_token nuevo sin volver a pedir la contraseña.
!

No programes contra una vigencia fija. Las dos difieren por entorno, y la de pruebas es corta a propósito: con 8 horas, renovar no es un caso excepcional que atiendes algún día, es algo que tu integración hace sola todos los días. Si la resuelves en pruebas, en producción ya está resuelta.

La forma correcta no es calcular cuándo caduca, sino reaccionar al 401: renueva con /auth/refresh y reintenta la llamada una vez. Así funciona igual si mañana cambiamos los plazos.

!

Guarda los tokens del lado del servidor. Un access_token da acceso a todos los documentos de tu empresa. No lo pongas en localStorage, en el código de tu front-end ni en un repositorio.

El token de tu aplicación#

!

En despliegue por entornos. Esta credencial se empieza a exigir en pruebas y después en producción, con aviso previo. Si ya tienes tu token de aplicación, mándalo desde ahora: donde todavía no se exige, se ignora sin romper nada.

Una integración presenta dos credenciales que responden preguntas distintas, y ninguna sustituye a la otra:

EncabezadoQué diceDe dónde sale
X-App-Token Qué aplicación está llamando. Te lo entrega 789 al dar de alta tu aplicación. Uno por entorno.
Authorization: Bearer En nombre de qué empresa. Lo obtienes con login, y cambia según la empresa.
Las dos, en cada petición
X-App-Token: mmapp_a1b2c3d4_kZ9rTq2WmNp4XeLs7VhB
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

El token de aplicación va acompañado de una lista de direcciones IP autorizadas: solo se acepta si la petición llega desde una de ellas. Si cambias de servidor o agregas uno, avísanos antes — desde una dirección no registrada la respuesta es 403 aunque el token sea correcto.

RespuestaQué pasó
401 {"detail": "Invalid or missing application token"}Falta el encabezado, o el token no es válido en este entorno. Revisa que no estés usando el de pruebas contra producción.
403 {"detail": "Client address is not on this application's allowlist"}El token sirve, pero la dirección desde la que llamas no está autorizada.
i

Un token por entorno, y no son intercambiables. El de producción solo funciona en /api/v2 y el de pruebas solo en /api/v2/test. Es deliberado: evita que una prueba escriba en producción por haber dejado la URL vieja en una variable de entorno.

!

Login y register no son excepciones. Son los dos endpoints que no pueden pedir el token de usuario —son la forma de obtenerlo—, pero sí exigen el de aplicación. Es justamente ahí donde importa: sin él, cualquiera que alcance el servidor podría intentar contraseñas o dar de alta empresas. El único endpoint sin credenciales es /health, que vive fuera de /api/v2.

Iniciar sesión#

POST /api/v2/auth/login Token de aplicación

Valida las credenciales y entrega el par de tokens.

CampoTipoDescripción
email requeridostringCorreo del usuario.
password requeridostringContraseña. Mínimo 8 caracteres.
Petición
curl -X POST "$MM/auth/login" \
  -H "X-App-Token: $APP_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "tu-usuario@empresa.mx",
    "password": "TuContrasena123"
  }'
$res = $http->post('auth/login', ['json' => [
    'email'    => 'tu-usuario@empresa.mx',
    'password' => 'TuContrasena123',
]]);
$auth = json_decode((string) $res->getBody(), true);
const auth = await fetch(`${MM}/auth/login`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: 'tu-usuario@empresa.mx',
    password: 'TuContrasena123',
  }),
}).then((r) => r.json());
auth = requests.post(f"{MM}/auth/login", json={
    "email": "tu-usuario@empresa.mx",
    "password": "TuContrasena123",
}, timeout=30).json()
200 OK
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "user": {
    "id": 12,
    "company_id": 3,
    "email": "tu-usuario@empresa.mx",
    "full_name": "Ana Ruiz",
    "role": "company_admin",
    "is_active": true,
    "last_login_at": "2026-09-18T14:02:11Z",
    "created_at": "2026-01-20T09:14:00Z",
    "updated_at": "2026-09-18T14:02:11Z"
  }
}

Renovar el token de acceso#

POST /api/v2/auth/refresh Token de aplicación

Cuando el access_token caduca, cámbialo por uno nuevo enviando el refresh_token en el cuerpo, no en el encabezado Authorization. Es el error más común al implementarlo, porque todos los demás endpoints funcionan al revés.

La respuesta trae solo un access_token. El refresh_token no se renueva ni se reemplaza: sigue siendo el mismo hasta que caduque por su cuenta, y cuando eso pase hay que volver a iniciar sesión con la contraseña.

i

Cuándo llamarlo. No calcules la caducidad: reacciona al 401. Una llamada devuelve 401 → renuevas aquí → reintentas esa misma llamada una vez. Si el refresh también devuelve 401, el refresh_token ya caducó y toca login.

Reintenta una vez, no en bucle. Si la segunda llamada vuelve a dar 401, el problema no es la caducidad y reintentar solo multiplica el tráfico.

Petición
curl -X POST "$MM/auth/refresh" \
  -H "X-App-Token: $APP_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}'
$res = $http->post('auth/refresh', [
    'json' => ['refresh_token' => $auth['refresh_token']],
]);
$token = json_decode((string) $res->getBody(), true)['access_token'];
const { access_token } = await fetch(`${MM}/auth/refresh`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ refresh_token: auth.refresh_token }),
}).then((r) => r.json());
token = requests.post(f"{MM}/auth/refresh", json={
    "refresh_token": auth["refresh_token"],
}, timeout=30).json()["access_token"]
200 OK
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}
CódigoCuándo ocurreRespuesta
401El refresh_token es inválido o caducó.{"detail": "Invalid or expired refresh token"}
401Enviaste un access_token en lugar del de refresco.{"detail": "Invalid token type. Expected refresh token"}
401El token no trae usuario, o el usuario ya no existe.{"detail": "Invalid token payload"} · {"detail": "User not found"}
403La cuenta fue desactivada.{"detail": "User account is inactive"}

Registrar una empresa#

POST /api/v2/auth/register Token de aplicación

Crea una empresa nueva junto con su usuario administrador, y devuelve ya el par de tokens. Es el endpoint que llama el panel de tu aplicación cuando das de alta a un cliente: guardas las credenciales que devuelve y a partir de ahí operas en nombre de esa empresa.

La empresa nace en el entorno de la URL que uses: /api/v2/auth/register la crea en producción, /api/v2/test/auth/register en pruebas. Son mundos separados, sin datos en común.

CampoTipoReglas
company_name requeridostringEntre 2 y 100 caracteres.
email requeridostringCorreo del administrador. Debe ser único.
password requeridostringMínimo 8 caracteres, con mayúscula, minúscula y dígito.
full_name requeridostringNombre del administrador.
relationship opcional enum owner si esta empresa es tu propia aplicación usando el servicio; subleased si es un cliente al que se lo revendes. Omitirlo equivale a subleased. Requiere token de aplicación.
i

Esto es lo que vincula la empresa con tu aplicación. Al registrarla queda registrada también como tuya, en la misma operación: no hay un paso aparte que se pueda olvidar, ni un momento en el que la empresa exista sin dueño.

Marca como owner la primera, la tuya. Solo puede haber una por aplicación y por entorno —una en producción y una en pruebas—; pedir una segunda responde 409 y no crea nada, ni la empresa. Las demás van como subleased, que es el valor por omisión, así que para ellas no necesitas mandar el campo.

Petición
curl -X POST "$MM/auth/register" \
  -H "X-App-Token: $APP_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "company_name": "Distribuidora Alamo",
    "email": "admin@alamo.mx",
    "password": "Alamo2026Seguro",
    "full_name": "Ana Ruiz",
    "relationship": "subleased"
  }'
$res = $http->post('auth/register', ['json' => [
    'company_name' => 'Distribuidora Alamo',
    'email'        => 'admin@alamo.mx',
    'password'     => 'Alamo2026Seguro',
    'full_name'    => 'Ana Ruiz',
]]);
const alta = await fetch(`${MM}/auth/register`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    company_name: 'Distribuidora Alamo',
    email: 'admin@alamo.mx',
    password: 'Alamo2026Seguro',
    full_name: 'Ana Ruiz',
  }),
}).then((r) => r.json());
alta = requests.post(f"{MM}/auth/register", json={
    "company_name": "Distribuidora Alamo",
    "email": "admin@alamo.mx",
    "password": "Alamo2026Seguro",
    "full_name": "Ana Ruiz",
}, timeout=30).json()
201 Created
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "company_id": 7,
  "company_name": "Distribuidora Alamo",
  "tenant": {
    "application_slug": "fleet",
    "relationship": "subleased"
  },
  "user": {
    "id": 21,
    "email": "admin@alamo.mx",
    "full_name": "Ana Ruiz",
    "role": "company_admin",
    "company_id": 7,
    "is_active": true,
    "created_at": "2026-09-18T14:10:00Z",
    "updated_at": "2026-09-18T14:10:00Z"
  }
}

Cerrar sesión#

POST /api/v2/auth/logout Token de aplicación + usuario

Revoca el access_token que envías en el encabezado. Deja de funcionar de inmediato, aunque todavía le quedara vigencia, y en todos los procesos de la API a la vez.

cURL
curl -X POST "$MM/auth/logout" \
  -H "X-App-Token: $APP_TOKEN" \
  -H "Authorization: Bearer $TOKEN"
i

Es idempotente: llamarlo dos veces, o con un token ya caducado, responde igual. No tienes que comprobar nada antes.

Revoca ese token, no la sesión. El refresh_token sigue sirviendo, así que quien lo tenga puede obtener uno nuevo. Para cortar de verdad a un usuario hay que desactivar su cuenta, y eso se hace desde el panel: surte efecto en la siguiente llamada, porque la API comprueba la cuenta en cada petición.

Si se te filtró un access_token, este es el corte inmediato. Llámalo y después avisa a soporte para revisar si hay que desactivar la cuenta.


Plantillas#

Una plantilla es tu archivo .docx con los campos combinables incrustados. MailMerge la analiza al subirla, extrae los nombres de los campos y te los devuelve: esa lista es el contrato entre tu documento de Word y el JSON que enviarás después.

Cómo preparar el .docx

  1. Abre tu documento en Microsoft Word.
  2. Coloca el cursor donde debe ir el dato variable.
  3. Ve a Insertar → Elementos rápidos → Campo…, elige MergeField y escribe el nombre del campo (por ejemplo nombre_cliente).
  4. Repite para cada dato. Puedes usar el mismo nombre las veces que quieras: se rellenará en todas.
  5. Guarda como .docx y súbelo con POST /templates.
i

Convenciones de nombres que activan funciones extra:

Un campo cuyo nombre empieza con img_, imagen_, image_, foto_, photo_ o pic_ se trata como imagen y espera una URL. Cualquier campo cuyo valor contenga etiquetas HTML se renderiza como texto con formato. No hay que configurar nada más.

Subir una plantilla#

POST /api/v2/templates Token de aplicación + usuario

Envío multipart/form-data. Extensiones aceptadas: .docx y .doc; máximo 50 MB.

Campo del formularioTipoDescripción
file requeridoarchivoEl documento de Word que sirve de plantilla.
ref_id requeridostringTu identificador de la plantilla. Úsalo para reconciliar con tu propia base de datos.
name requeridostringNombre legible de la plantilla.
template_type requeridoenumcontract (documento principal) o annex (anexo).
Petición
curl -X POST "$MM/templates" \
  -H "Authorization: Bearer $TOKEN" \
  -F 'file=@contrato.docx' \
  -F 'ref_id=CONTRATO-BASE' \
  -F 'name=Contrato de arrendamiento' \
  -F 'template_type=contract'
$res = $http->post('templates', [
    'headers'   => $auth_header,
    'multipart' => [
        ['name' => 'file',          'contents' => fopen('contrato.docx', 'r')],
        ['name' => 'ref_id',        'contents' => 'CONTRATO-BASE'],
        ['name' => 'name',          'contents' => 'Contrato de arrendamiento'],
        ['name' => 'template_type', 'contents' => 'contract'],
    ],
]);
$plantilla = json_decode((string) $res->getBody(), true);
const form = new FormData();
form.append('file', archivo, 'contrato.docx');   // File o Blob
form.append('ref_id', 'CONTRATO-BASE');
form.append('name', 'Contrato de arrendamiento');
form.append('template_type', 'contract');

const plantilla = await fetch(`${MM}/templates`, {
  method: 'POST', headers: auth, body: form,
}).then((r) => r.json());
with open("contrato.docx", "rb") as f:
    plantilla = requests.post(
        f"{MM}/templates",
        headers=auth,
        files={"file": ("contrato.docx", f)},
        data={
            "ref_id": "CONTRATO-BASE",
            "name": "Contrato de arrendamiento",
            "template_type": "contract",
        },
        timeout=60,
    ).json()
201 Created
{
  "id": 550,
  "company_id": 3,
  "ref_id": "CONTRATO-BASE",
  "name": "Contrato de arrendamiento",
  "template_type": "contract",
  "file_path": "/uploads/company_3/templates/8f2c…-contrato.docx",
  "original_filename": "contrato.docx",
  "file_size": 48213,
  "merge_fields": [
    "nombre_cliente",
    "fecha_contrato",
    "monto_total"
  ],
  "version": 1,
  "is_active": true,
  "created_by": 12,
  "created_at": "2026-09-18T14:21:03Z",
  "updated_at": "2026-09-18T14:21:03Z"
}
!

Revisa siempre merge_fields. Es la verdad sobre lo que MailMerge encontró dentro del .docx. Si esperabas monto_total y aparece MontoTotal, el campo se insertó con otro nombre en Word: corrígelo ahí o ajusta tu JSON. Un campo que envías y no existe en la plantilla se ignora en silencio.

Listar plantillas#

GET /api/v2/templates Token de aplicación + usuario

Devuelve un arreglo con las plantillas de tu empresa.

Parámetro (query)TipoDescripción
template_type opcionalenumFiltra por contract o annex.
is_active opcionalbooleanPor omisión true: solo plantillas vigentes.
Petición
curl "$MM/templates?template_type=contract&is_active=true" \
  -H "Authorization: Bearer $TOKEN"
$res = $http->get('templates', [
    'headers' => $auth_header,
    'query'   => ['template_type' => 'contract', 'is_active' => 'true'],
]);
$plantillas = json_decode((string) $res->getBody(), true);
const q = new URLSearchParams({ template_type: 'contract', is_active: 'true' });
const plantillas = await fetch(`${MM}/templates?${q}`, { headers: auth })
  .then((r) => r.json());
plantillas = requests.get(f"{MM}/templates", headers=auth, params={
    "template_type": "contract",
    "is_active": True,
}, timeout=30).json()

Consultar una plantilla#

GET /api/v2/templates/{template_id} Token de aplicación + usuario

Devuelve la misma estructura que la respuesta de subida. Es la forma recomendada de recuperar merge_fields antes de armar el JSON de generación.

cURL
curl "$MM/templates/550" -H "Authorization: Bearer $TOKEN"

Eliminar una plantilla#

DELETE /api/v2/templates/{template_id} Token de aplicación + usuario · rol administrador

Baja lógica: la plantilla queda como is_active: false y deja de aparecer en los listados, pero los documentos ya generados con ella conservan su historial. Responde 204 No Content.

cURL
curl -X DELETE "$MM/templates/550" -H "Authorization: Bearer $TOKEN" -i

Documentos#

Aquí es donde tu aplicación pasa la mayor parte del tiempo: una llamada para generar, una para descargar.

Generar un documento#

POST /api/v2/documents Token de aplicación + usuario

Combina una plantilla con tus datos y deja el archivo listo para descargar.

CampoTipoDescripción
template_id requeridointegerEl id que te devolvió la subida de la plantilla.
ref_id requeridostringTu referencia del documento: número de contrato, folio, id de expediente…
merge_data requeridoobjectLos datos. Ver las tres claves siguientes.
merge_data.fields opcionalobjectPares nombre_campo: valor para los campos sueltos.
merge_data.tables opcionalarrayFilas repetidas. Ver tablas. Máximo 50 tablas.
merge_data.images opcionalobjectImágenes con dimensiones explícitas. Ver imágenes.
output_format opcionalenumpdf (por omisión) o docx.
Petición
curl -X POST "$MM/documents" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "template_id": 550,
    "ref_id": "CONTRATO-2026-0187",
    "merge_data": {
      "fields": {
        "nombre_cliente": "Distribuidora Alamo S.A. de C.V.",
        "fecha_contrato": "18/09/2026",
        "monto_total": "$128,400.00"
      }
    },
    "output_format": "pdf"
  }'
$res = $http->post('documents', [
    'headers' => $auth_header,
    'json'    => [
        'template_id' => 550,
        'ref_id'      => 'CONTRATO-2026-0187',
        'merge_data'  => [
            'fields' => [
                'nombre_cliente' => 'Distribuidora Alamo S.A. de C.V.',
                'fecha_contrato' => '18/09/2026',
                'monto_total'    => '$128,400.00',
            ],
        ],
        'output_format' => 'pdf',
    ],
]);
$documento = json_decode((string) $res->getBody(), true);
const documento = await fetch(`${MM}/documents`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    template_id: 550,
    ref_id: 'CONTRATO-2026-0187',
    merge_data: {
      fields: {
        nombre_cliente: 'Distribuidora Alamo S.A. de C.V.',
        fecha_contrato: '18/09/2026',
        monto_total: '$128,400.00',
      },
    },
    output_format: 'pdf',
  }),
}).then((r) => r.json());
documento = requests.post(f"{MM}/documents", headers=auth, json={
    "template_id": 550,
    "ref_id": "CONTRATO-2026-0187",
    "merge_data": {
        "fields": {
            "nombre_cliente": "Distribuidora Alamo S.A. de C.V.",
            "fecha_contrato": "18/09/2026",
            "monto_total": "$128,400.00",
        },
    },
    "output_format": "pdf",
}, timeout=120).json()
201 Created
{
  "id": 1234,
  "company_id": 3,
  "template_id": 550,
  "ref_id": "CONTRATO-2026-0187",
  "status": "completed",
  "file_path": "/downloads/company_3/1f9c…-contrato.pdf",
  "file_size": 186342,
  "download_url": "/api/v2/documents/1234/download",
  "error_message": null,
  "task_id": null,
  "expires_at": "2026-09-25T14:31:44Z",
  "created_at": "2026-09-18T14:31:40Z",
  "completed_at": "2026-09-18T14:31:44Z"
}
i

Guarda el id, no la ruta. file_path es interno y puede cambiar; la manera estable de recuperar el archivo es /api/v2/documents/{id}/download. Y ten presente expires_at: el archivo se conserva 7 días. Si tu sistema necesita el documento a largo plazo, guárdalo en tu propio almacenamiento al descargarlo.

Descargar un documento#

GET /api/v2/documents/{document_id}/download Token de aplicación + usuario

Devuelve el archivo binario, no JSON.

Encabezado de respuestaValor
Content-Typeapplication/pdf o application/vnd.openxmlformats-officedocument.wordprocessingml.document
Content-Dispositionattachment; filename="CONTRATO-2026-0187.pdf"
Petición
curl "$MM/documents/1234/download" \
  -H "Authorization: Bearer $TOKEN" \
  -o contrato-2026-0187.pdf
$http->get('documents/1234/download', [
    'headers' => $auth_header,
    'sink'    => 'contrato-2026-0187.pdf',
]);
const res = await fetch(`${MM}/documents/1234/download`, { headers: auth });
const blob = await res.blob();      // en el navegador
// en Node: await writeFile('doc.pdf', Buffer.from(await res.arrayBuffer()));
pdf = requests.get(f"{MM}/documents/1234/download", headers=auth, timeout=120)
pdf.raise_for_status()
with open("contrato-2026-0187.pdf", "wb") as f:
    f.write(pdf.content)

Si el documento no existe, no es de tu empresa o todavía no está terminado, la respuesta es 404 con {"error": "Document not found or not ready for download"}.

Consultar el estado#

GET /api/v2/documents/{document_id}/status Token de aplicación + usuario

Ligero y pensado para polling. En el flujo normal no lo necesitas, porque la generación termina antes de que responda POST /documents. Úsalo si guardas el id y quieres confirmar el resultado desde otro proceso.

200 OK
{
  "id": 1234,
  "status": "completed",
  "created_at": "2026-09-18T14:31:40Z",
  "updated_at": "2026-09-18T14:31:44Z"
}
EstadoSignificado
queuedRegistrado, aún sin procesar.
processingCombinación y conversión en curso.
completedListo para descargar.
failedFalló. El motivo viene en error_message al consultar el documento.

Listar documentos#

GET /api/v2/documents Token de aplicación + usuario
Parámetro (query)TipoDescripción
template_id opcionalintegerSolo documentos de esa plantilla.
ref_id opcionalstringBúsqueda por tu referencia.
status opcionalenumqueued, processing, completed o failed.
limit opcionalintegerPor omisión 100, máximo 1000.
offset opcionalintegerDesplazamiento para paginar. Por omisión 0.
cURL
curl "$MM/documents?ref_id=CONTRATO-2026-0187&status=completed&limit=20" \
  -H "Authorization: Bearer $TOKEN"

Consultar un documento#

GET /api/v2/documents/{document_id} Token de aplicación + usuario

Devuelve los metadatos completos del documento, con la misma forma que la respuesta de creación. Es donde consultas error_message cuando un documento quedó en failed.


Tablas repetidas#

Para listas de longitud variable —conceptos de una cotización, pagos de una amortización, anexos— se usa merge_data.tables. En Word, inserta una fila de tabla con los campos combinables de cada columna; MailMerge la duplica tantas veces como filas envíes.

CampoTipoDescripción
first_field requeridostringNombre del campo de la primera columna. Es la ancla que identifica la fila a repetir.
rows requeridoarrayUn objeto por fila, con una clave por columna. Máximo 1000 filas.
Campos sueltos + una tabla
{
  "template_id": 551,
  "ref_id": "COT-2026-0442",
  "merge_data": {
    "fields": {
      "numero_cotizacion": "COT-2026-0442",
      "cliente": "Distribuidora Alamo S.A. de C.V.",
      "total": "$46,400.00"
    },
    "tables": [
      {
        "first_field": "concepto",
        "rows": [
          { "concepto": "Instalación",   "cantidad": "1",  "precio": "$28,000.00" },
          { "concepto": "Capacitación",  "cantidad": "2",  "precio": "$12,000.00" },
          { "concepto": "Soporte anual", "cantidad": "12", "precio": "$6,400.00"  }
        ]
      }
    ]
  },
  "output_format": "pdf"
}
!

Todas las filas deben traer las mismas claves. Si una fila omite una columna, esa celda queda vacía en el documento. Y first_field tiene que ser exactamente el campo de la primera columna de la tabla en Word; si no coincide, la tabla no se repite y se rellena una sola fila.

Puedes enviar hasta 50 tablas en un mismo documento: agrega un objeto más al arreglo tables, cada uno con su propio first_field.


Imágenes por URL#

MailMerge puede descargar una imagen de una URL pública e insertarla en el documento, conservando la proporción. Hay dos formas de pedirlo.

Por convención de nombre (lo habitual)

Si el campo de tu plantilla empieza con uno de estos prefijos, basta con enviar la URL como valor en fields:

PrefijoEjemplos de nombre de campo
img_img_logo, img_firma
imagen_imagen_producto, imagen_firma
image_image_header, image_footer
foto_foto_inmueble, foto_perfil
photo_photo_product
pic_pic_banner
Imágenes por prefijo
{
  "fields": {
    "cliente": "Distribuidora Alamo",
    "img_logo_empresa": "https://cdn.tuempresa.mx/logos/alamo.png",
    "imagen_producto": "https://cdn.tuempresa.mx/productos/8842.jpg",
    "img_opcional": ""
  }
}

Para una imagen que puede no existir —por ejemplo la foto de un activo en una cotización— envía "", null o "N/A": el marcador se elimina limpiamente del documento en lugar de dejar un hueco.

Con dimensiones explícitas

Cuando necesitas control del tamaño o la alineación, usa merge_data.images:

Imágenes con configuración
{
  "merge_data": {
    "fields": { "cliente": "Distribuidora Alamo" },
    "images": {
      "logo": {
        "url": "https://cdn.tuempresa.mx/logos/alamo.png",
        "max_width_inches": 2.0,
        "max_height_inches": 1.5,
        "fit_mode": "contain",
        "position": "center",
        "alt_text": "Logotipo de Distribuidora Alamo"
      },
      "firma": "https://cdn.tuempresa.mx/firmas/ana-ruiz.png"
    }
  }
}
PropiedadTipoPor omisiónDescripción
url requeridostring—URL pública de la imagen. El servidor debe poder descargarla.
max_width_inchesnumber6.0Ancho máximo en pulgadas (0.1–11).
max_height_inchesnumber8.0Alto máximo en pulgadas (0.1–14).
width_inchesnumber—Ancho exacto; tiene prioridad sobre max_width_inches.
height_inchesnumber—Alto exacto; tiene prioridad sobre max_height_inches.
fit_modeenumcontaincontain, cover, stretch u original.
positionenumleftAlineación horizontal: left, center o right.
alt_textstring—Texto alternativo, hasta 500 caracteres.
i

Las URLs deben ser accesibles públicamente desde internet, sin autenticación. Si la descarga falla o tarda demasiado, el documento se genera sin esa imagen en lugar de fallar por completo.


Contenido HTML con formato#

Un campo puede traer HTML y MailMerge lo convierte a formato real de Word: listas numeradas, viñetas, negritas, tablas. Es la forma de insertar texto con estructura sin tener que prever cada variante en la plantilla. No hay que marcar nada: si el valor contiene etiquetas HTML, se procesa como tal.

Campo con HTML
{
  "fields": {
    "cliente": "Distribuidora Alamo",
    "modo_pago": "<ol type=\"a\"><li>Anticipo: $64,224.14</li><li>Gestoria: $9,444.82</li></ol>",
    "observaciones": "<p>Vigencia de <b>30 dias</b> naturales.</p>"
  }
}

Etiquetas admitidas

GrupoEtiquetas
Listas<ul>, <ol>, <li> (admiten anidamiento)
Tablas<table>, <tr>, <td>, <th>
Formato<b>, <strong>, <i>, <em>, <u>, <s>
Estructura<p>, <br>, <h1>–<h6>, <hr>
Otros<a>, <blockquote>, <pre>, <code>

Numeración de listas ordenadas

El atributo type de <ol> controla el estilo de numeración:

typeResultado
1 (por omisión)1. 2. 3.
aa. b. c.
AA. B. C.
ii. ii. iii.
II. II. III.
!

Escapa el HTML dentro del JSON. Las comillas de los atributos tienen que ir como \". Si armas el JSON con la librería de tu lenguaje (y no concatenando cadenas), esto se resuelve solo.


Utilidades PDF#

Independientes de la combinación de correspondencia: sirven para unir archivos ya existentes en un solo PDF —por ejemplo el contrato firmado más sus anexos escaneados—.

Unir archivos en un PDF

POST /api/v2/pdf/merge Token de aplicación + usuario

Envías un ZIP con los archivos a unir, en multipart/form-data. Se combinan en el orden alfabético de sus nombres dentro del ZIP, así que conviene prefijarlos: 01-contrato.pdf, 02-anexo.pdf…

ParámetroDóndeDescripción
file requeridoformularioArchivo ZIP con los documentos o imágenes.
output_filename opcionalformularioNombre del PDF resultante, sin la extensión.
strict_mode opcionalqueryfalse por omisión. En true, un archivo inválido aborta la operación.
detailed opcionalqueryfalse por omisión. En true devuelve JSON con el detalle en lugar del PDF.
cURL
curl -X POST "$MM/pdf/merge?strict_mode=false" \
  -H "Authorization: Bearer $TOKEN" \
  -F 'file=@expediente.zip' \
  -F 'output_filename=expediente-2026-0187' \
  -o expediente-2026-0187.pdf

Validar archivos antes de unirlos

POST /api/v2/pdf/validate Token de aplicación + usuario

Recibe uno o más archivos (campo files, repetible) y responde con el diagnóstico de cada uno y si el conjunto es unible —hacen falta al menos dos archivos válidos—. No genera nada.

Formatos admitidos

GET /api/v2/pdf/formats Token de aplicación + usuario

Devuelve la lista viva de extensiones y tipos MIME que aceptan los endpoints de PDF, junto con los límites vigentes. Consúltala desde tu aplicación en vez de codificar la lista a mano: incluye PDF, XPS, EPUB, CBZ, SVG e imágenes.

cURL
curl "$MM/pdf/formats"

Usuarios y roles#

Cada usuario pertenece a una empresa y solo ve los datos de esa empresa. La API expone el alta y gestión de usuarios para que tu aplicación pueda administrarlos sin entrar al panel.

RolPuede
userSubir plantillas, generar y descargar documentos de su empresa.
company_adminTodo lo anterior, más crear, modificar y desactivar usuarios de su empresa y eliminar plantillas.
super_adminAdministración de la plataforma. Reservado a 789.
MétodoRutaPermisoDescripción
POST/api/v2/usersAdministradorCrea un usuario en tu empresa. Cuerpo: email, password, full_name, role.
GET/api/v2/usersAdministradorLista los usuarios. Filtros: is_active, role.
GET/api/v2/users/{user_id}AutenticadoConsulta un usuario.
PATCH/api/v2/users/{user_id}Administrador o el propio usuarioActualiza datos del usuario.
DELETE/api/v2/users/{user_id}AdministradorDesactiva la cuenta (baja lógica). Nadie puede eliminarse a sí mismo.

Salud y diagnóstico#

Dos son públicos y dos no. La diferencia importa: un monitor externo puede llamar a /health sin credenciales, mientras que /api/v2/ping exige las dos y por eso sirve para lo contrario — comprobar que tu integración está completa.

MétodoRutaCredencialesRespuesta
GET/healthNinguna{"status":"healthy", …}
GET/openapi.jsonNingunaEspecificación OpenAPI 3.1, para generar clientes.
GET/api/v2/pingApp + usuario{"message":"pong"}
GET/api/v2/App + usuarioVersión de la API y mapa de sus secciones.
✓

/api/v2/ping es tu primera llamada. Es la más barata que existe y atraviesa todo: token de aplicación, allowlist de direcciones y token de usuario. Si responde pong, lo demás es cuestión de armar bien los JSON.

i

Genera tu cliente automáticamente. Como la especificación está publicada, puedes producir un SDK tipado sin escribirlo a mano:

npx @openapitools/openapi-generator-cli generate -i https://mailmerge.789.com.mx/openapi.json -g typescript-fetch -o ./mailmerge-client


Todos los endpoints#

Referencia completa de la API v2. Todas las rutas van precedidas de https://mailmerge.789.com.mx. La columna de credenciales dice qué hay que presentar: app es el token de aplicación, usuario es el Authorization: Bearer que devuelve el login.

MétodoRutaCredencialesQué hace
POST/api/v2/auth/registerappRegistra una empresa con su administrador.
POST/api/v2/auth/loginappEntrega el par de tokens.
POST/api/v2/auth/refreshappRenueva el token de acceso.
POST/api/v2/auth/logoutapp + usuarioRevoca el token de acceso presentado.
POST/api/v2/templatesapp + usuarioSube una plantilla .docx y detecta sus campos.
GET/api/v2/templatesapp + usuarioLista las plantillas de la empresa.
GET/api/v2/templates/{template_id}app + usuarioConsulta una plantilla y sus merge_fields.
DELETE/api/v2/templates/{template_id}app + usuario · adminDa de baja lógica una plantilla.
POST/api/v2/documentsapp + usuarioGenera un documento a partir de una plantilla y datos.
GET/api/v2/documentsapp + usuarioLista documentos con filtros y paginación.
GET/api/v2/documents/{document_id}app + usuarioMetadatos de un documento.
GET/api/v2/documents/{document_id}/statusapp + usuarioEstado de la generación.
GET/api/v2/documents/{document_id}/downloadapp + usuarioDescarga el archivo generado.
POST/api/v2/usersapp + usuario · adminCrea un usuario.
GET/api/v2/usersapp + usuario · adminLista usuarios.
GET/api/v2/users/{user_id}app + usuarioConsulta un usuario.
PATCH/api/v2/users/{user_id}app + usuarioActualiza un usuario.
DELETE/api/v2/users/{user_id}app + usuario · adminDesactiva un usuario.
POST/api/v2/pdf/mergeapp + usuarioUne los archivos de un ZIP en un solo PDF.
POST/api/v2/pdf/validateapp + usuarioValida archivos antes de unirlos.
GET/api/v2/pdf/formatsapp + usuarioFormatos y límites admitidos.
GET/api/v2/app + usuarioInformación de la API.
GET/api/v2/pingapp + usuarioPrueba de conectividad.
GET/healthningunaEstado del servicio.

Errores#

La API responde con códigos HTTP estándar. Los errores traen un cuerpo JSON: detail con el motivo —una cadena, o un arreglo cuando se trata de validación—.

CódigoSignificadoQué hacer
200 / 201Todo bien.—
204Eliminación correcta, sin cuerpo.—
400Petición mal formada.Revisa el cuerpo y los tipos de dato.
401Falta el token, es inválido o caducó.Renueva con /auth/refresh; si también falla, vuelve a iniciar sesión.
403Sin permisos suficientes, o cuenta desactivada.Verifica el rol del usuario.
404No existe o es de otra empresa.Confirma el identificador. El aislamiento entre empresas devuelve 404, no 403.
409Conflicto: ya existe una plantilla con ese ref_id y tipo.Reutiliza la plantilla existente o usa otro ref_id.
422Error de validación del cuerpo.Lee detail[].loc para saber qué campo falló.
500Error del servidor.Reintenta con espera exponencial. Si persiste, avisa a soporte con el ref_id.

Ejemplos reales

401 — falta el encabezado Authorization
{
  "detail": "Missing or invalid authorization header"
}
422 — la contraseña no cumple la validación
{
  "detail": [
    {
      "type": "string_too_short",
      "loc": ["body", "password"],
      "msg": "String should have at least 8 characters",
      "ctx": { "min_length": 8 }
    }
  ]
}
!

Un documento puede fallar sin que la petición falle. Si status queda en failed, la llamada HTTP puede haber respondido correctamente. Revisa siempre status en la respuesta y, cuando sea failed, lee error_message.

Límites y cuotas#

ConceptoLímite
Tamaño de archivo subido50 MB
Extensiones de plantilla.docx, .doc
Tablas por documento50
Filas por tabla1000
Conservación del documento generado7 días (expires_at)
Resultados por página en listados100 por omisión, 1000 máximo
Vigencia del access_token8 horas en pruebas · 30 días en producción
Vigencia del refresh_token7 días en pruebas · 182 días en producción
Dimensiones máximas de imagen11 × 14 pulgadas

No hay un límite de peticiones por minuto publicado. Aun así, si vas a generar documentos en lote, hazlo de forma secuencial o con poca concurrencia: cada generación implica convertir un documento de Word, y las ráfagas grandes degradan el tiempo de respuesta para todos.

Buenas prácticas#


Soporte#

¿Necesitas credenciales, dar de alta tu empresa o resolver un caso concreto? Escríbenos a soporte@789.mx. Para reportar un problema de generación, incluye el ref_id del documento y la hora aproximada: con eso lo ubicamos en los registros.

EXPLORAR

Swagger UI

Prueba cualquier endpoint desde el navegador, con tu propio token.

AUTOMATIZAR

OpenAPI 3.1

La especificación completa, para generar clientes en tu lenguaje.

MONITOREAR

Estado del servicio

Comprobación de salud pública, apta para tus alertas.