Documentación de la API de EasySigner
firma-api recibe un documento PDF, lo firma digitalmente con el certificado
digital de su entidad, y devuelve el resultado. No genera documentos,
no tiene interfaz web — todo se usa por API, pensado para que su propio sistema (gobierno,
educativo, bufete, etc.) firme sin tener que construir un firmador propio.
/scalar (con "Try it" desde el navegador) y el contrato crudo en
/openapi/v1.json, para importar a Postman o generar un cliente. Esta página
es la guía general — reemplace <su-instancia> por la URL que les dimos.
El certificado con el que se firma es el de ustedes: la firma tiene la validez legal del sello de su entidad, no de un tercero.
Antes de empezar
Nuestro equipo les entrega, por un canal seguro (nunca por esta página):
| Dato | Para qué sirve |
|---|---|
client_id + client_secret | Autenticarse. El client_secret se muestra una sola vez al crearlo. |
| URL base de su instancia | Ej. https://api-cliente-x.ejemplo.com. Todas las rutas de acá son relativas a esa URL. |
| Saldo de créditos cargado | Cada documento firmado gasta 1 crédito. Sin saldo, la API responde 402. |
Por su lado, definan antes de integrar: si van a usar webhook, polling, o ambos, y si sus PDFs llevan firma visible o invisible.
Ejemplo completo, de punta a punta
Un caso típico: pedir un token, mandar un PDF a firmar (visible, con QR), y enterarse del resultado por webhook. Cada bloque gris se puede copiar y pegar tal cual en una terminal — solo cambien el dominio y las credenciales por las suyas.
1. Pedir el token
application/x-www-form-urlencoded
(los flags -d de curl), no como JSON — es el
estándar de OAuth2 "client credentials", no una elección nuestra. El resto de la API sí
usa JSON para las respuestas (y multipart/form-data para subir archivos,
como en el paso 2).
curl -X POST https://api-municipalidad-central.ejemplo.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=ca_4f9e2a1b7c3d \
-d client_secret=8e1c2f6a9b7d4e3f0a1b2c3d4e5f6a7b
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRlX2lkIjoi...",
"token_type": "Bearer",
"expires_in": 900
}
2. Mandar el PDF a firmar
Reemplacen <access_token> por el que recibieron en el paso 1.
curl -X POST https://api-municipalidad-central.ejemplo.com/api/v1/firmas \
-H "Authorization: Bearer <access_token>" \
-F "[email protected]" \
-F "pin=4821" \
-F "visible=true" \
-F "modo_sello=piezas_sueltas" \
-F "position=50,40,260,100" \
-F "qr_url=https://validar.municipalidad-central.ejemplo.com/v/a1b2c3d4" \
-F "external_id=constancia-1842"
{
"job_id": "7e3a1f9c-2b4d-4e8a-9c1f-5d6e7a8b9c0d",
"external_id": "constancia-1842",
"status": "recibido"
}
3. Se enteran de que terminó (webhook)
Unos segundos después, si configuraron callback_url, su propio endpoint recibe
este POST de nuestra parte — esto no lo copian ustedes, lo reciben:
Content-Type: application/json
X-Firma-Signature: 5f8a3c1e9b7d2f4a6c8e0b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a
{
"job_id": "7e3a1f9c-2b4d-4e8a-9c1f-5d6e7a8b9c0d",
"external_id": "constancia-1842",
"status": "completado",
"error": null,
"hash_sha256": "9c1f5d6e7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d",
"documento_url": "/api/v1/firmas/7e3a1f9c-2b4d-4e8a-9c1f-5d6e7a8b9c0d/documento"
}
Deben responder 2xx rápido, y antes de hacer nada con el contenido, verificar X-Firma-Signature (ver Webhooks).
4. Descargar el documento firmado
curl https://api-municipalidad-central.ejemplo.com/api/v1/firmas/7e3a1f9c-2b4d-4e8a-9c1f-5d6e7a8b9c0d/documento \
-H "Authorization: Bearer <access_token>" \
-o constancia-1842-firmada.pdf
Content-Type: application/pdf
Content-Disposition: attachment; filename="constancia-1842-firmada.pdf"
<PDF firmado>
1. Autenticarse
OAuth2 "client credentials". El token dura 15 minutos; pídanlo de nuevo cuando venza.
application/x-www-form-urlencoded (los flags -d),
no como JSON — así lo pide el estándar OAuth2. El resto de la API sí
responde en JSON.
curl -X POST https://<su-instancia>/oauth/token \
-d grant_type=client_credentials \
-d client_id=ca_xxxxxxxxxxxx \
-d client_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 900
}
De acá en adelante, toda llamada a /api/v1/* lleva Authorization: Bearer <access_token>.
client_secret es tan sensible como una contraseña. Guárdenlo en su gestor
de secretos — nunca en el código fuente ni en un repositorio.
2. Firmar un documento
curl -X POST https://<su-instancia>/api/v1/firmas \
-H "Authorization: Bearer <access_token>" \
-F "[email protected]" \
-F "pin=1234" \
-F "visible=false"
| Campo | Obligatorio | Qué es |
|---|---|---|
file | Sí | El PDF a firmar. |
pin | Sí | PIN del certificado digital de su entidad. No lo guarden nunca de su lado tampoco. |
visible | No (default false) | true para dejar un recuadro de firma visible. |
modo_sello | Solo si visible=true | piezas_sueltas (dibujamos el recuadro) o ya_renderizado (su PDF ya lo trae). |
position | Solo si visible=true | "x1,y1,x2,y2" en puntos PDF, origen abajo-izquierda. |
page | No (default 0) | Página del recuadro (la primera es 0). |
qr_url | No | URL que codifica el QR (solo piezas_sueltas). |
imagen_firma | No | PNG de la firma (solo piezas_sueltas). |
modo | No (default asincrono) | asincrono o sincrono. |
external_id | No | Su propio identificador — útil para no firmar dos veces por un reintento. |
{
"job_id": "3f1b...",
"external_id": "su-id",
"status": "recibido"
}
3. Síncrono vs. asíncrono
Asíncrono (default, recomendado)
La API responde apenas recibe el documento. Se enteran por webhook o polling. Entre 5 y 10 segundos de punta a punta, sin tiempo fijo garantizado.
Síncrono (modo=sincrono)
La conexión espera y, si se confirma a tiempo (tope ~25-30s), el PDF firmado vuelve directo en el cuerpo. Si se pasa el tope, cae al mismo 202 asíncrono — su código tiene que manejar ambos casos igual.
4. Recibir el aviso por webhook (opcional)
Si configuraron una callback_url, en cuanto el documento termina (bien o mal) les llega — esto lo reciben, no lo mandan:
Content-Type: application/json
X-Firma-Signature: <hmac-sha256 hex>
{
"job_id": "...",
"status": "completado",
"hash_sha256": "...",
"documento_url": "/api/v1/firmas/3f1b.../documento"
}
Verifiquen siempre la firma antes de confiar en el contenido:
const crypto = require("crypto");
function esFirmaValida(cuerpoCrudo, firmaRecibida, webhookSecret) {
const esperada = crypto.createHmac("sha256", webhookSecret).update(cuerpoCrudo).digest("hex");
return crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(firmaRecibida));
}
Su endpoint debe ser HTTPS y responder 2xx rápido. Es best-effort — el polling con GET /api/v1/firmas/{job_id} siempre funciona aunque el webhook falle.
5. Consultar estado y descargar
curl https://<su-instancia>/api/v1/firmas/3f1b... \
-H "Authorization: Bearer <access_token>"
{
"job_id": "3f1b...",
"status": "recibido" | "en_proceso" | "completado" | "error",
"hash_sha256": null,
"documento_url": null
}
Cuando status es completado, descargar:
curl https://<su-instancia>/api/v1/firmas/3f1b.../documento \
-H "Authorization: Bearer <access_token>" -o firmado.pdf
409 si todavía no está firmado. 410 si ya pasó la retención (90 días) y el archivo se borró — el registro sigue existiendo para auditoría.
6. Firmar varios documentos (lote)
Mismo PIN para todos, un archivo y una posición por documento. Nunca es síncrono.
curl -X POST https://<su-instancia>/api/v1/firmas/lote \
-H "Authorization: Bearer <access_token>" \
-F "pin=1234" \
-F 'documentos=[{"external_id":"a","visible":false},{"external_id":"b","visible":false}]' \
-F "[email protected]" \
-F "[email protected]"
{
"lote_id": "...",
"documentos": [
{ "job_id": "...", "external_id": "a", "status": "recibido" },
{ "job_id": "...", "external_id": "b", "status": "recibido" }
]
}
Un documento que falla no afecta a los demás — cada uno tiene su propio job_id. Resumen del lote:
curl https://<su-instancia>/api/v1/firmas/lote/<lote_id> \
-H "Authorization: Bearer <access_token>"
{
"lote_id": "...",
"total": 2,
"completados": 1,
"en_proceso": 0,
"con_error": 1,
"documentos": [ ]
}
Hay un máximo de documentos por lote — si lo exceden, 400 antes de procesar nada.
7. Errores
| Código | Significa | Qué hacer |
|---|---|---|
| 400 | Falta un campo o el valor no es válido. | Revisar el cuerpo (el mensaje dice qué falta). |
| 401 | Token vencido o inválido. | Pedir un token nuevo. |
| 402 | Sin crédito suficiente. | Avisarnos para cargar más saldo. |
| 404 | El id no existe o no es de ustedes. | Revisar el id. |
| 409 | Todavía no terminó de firmarse. | Reintentar la descarga más tarde. |
| 410 | Se borró por retención (90 días). | El registro sigue existiendo, el archivo no. |
| 429 | Superaron el límite de peticiones. | Esperar y reintentar con backoff. |
| 502 | El proveedor de certificación rechazó la firma. | No reintentar un PIN incorrecto solos — puede bloquear el certificado. |
El mensaje de error siempre es genérico a propósito; el detalle real queda en nuestra auditoría interna.
8. Límites de uso
Por client_id, no por IP:
| Acción | Límite |
|---|---|
| Firmar (individual o lote) | 30 peticiones/minuto |
| Consultar estado / descargar | 120 peticiones/minuto |
| Pedir token | 10 peticiones/minuto por IP |
Al superarlo, 429. Si su volumen esperado es mayor, avísennos antes de integrar.
9. Checklist antes de producción
client_secretguardado en un secreto del servidor, no en el código.- El PIN se pide a una persona en el momento de firmar, nunca se guarda.
- Manejan los dos casos del modo síncrono (PDF directo o
202). - Si usan webhook, verifican
X-Firma-Signaturey el polling sigue funcionando igual. - Manejan
429con backoff, no con reintento inmediato. - Ante
502con PIN incorrecto, confirman con la persona autorizante antes de reintentar. - Usan
external_idpara no firmar dos veces el mismo documento.
Soporte
Cualquier duda de integración, más crédito, otro límite de uso, o cambiar la callback_url: contacten a nuestro equipo. El alta, baja y configuración de cada cliente la hacemos nosotros.