EasySigner API
Documentación v1
API REST · OAuth2 · JSON

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.

Cada instancia expone además su propia documentación interactiva en /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):

DatoPara qué sirve
client_id + client_secretAutenticarse. El client_secret se muestra una sola vez al crearlo.
URL base de su instanciaEj. https://api-cliente-x.ejemplo.com. Todas las rutas de acá son relativas a esa URL.
Saldo de créditos cargadoCada 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

Este endpoint en particular manda los datos como 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).
POST Request
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
200 OK Response
{
  "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.

POST Request
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"
202 Accepted Response
{
  "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:

POST Lo que les llega
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

GET Request
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
200 OK Response
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.

Se manda como 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.
POST Request
curl -X POST https://<su-instancia>/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=ca_xxxxxxxxxxxx \
  -d client_secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
200 OK Response
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 900
}

De acá en adelante, toda llamada a /api/v1/* lleva Authorization: Bearer <access_token>.

El 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

POST Request
curl -X POST https://<su-instancia>/api/v1/firmas \
  -H "Authorization: Bearer <access_token>" \
  -F "[email protected]" \
  -F "pin=1234" \
  -F "visible=false"
CampoObligatorioQué es
fileSíEl PDF a firmar.
pinSíPIN del certificado digital de su entidad. No lo guarden nunca de su lado tampoco.
visibleNo (default false)true para dejar un recuadro de firma visible.
modo_selloSolo si visible=truepiezas_sueltas (dibujamos el recuadro) o ya_renderizado (su PDF ya lo trae).
positionSolo si visible=true"x1,y1,x2,y2" en puntos PDF, origen abajo-izquierda.
pageNo (default 0)Página del recuadro (la primera es 0).
qr_urlNoURL que codifica el QR (solo piezas_sueltas).
imagen_firmaNoPNG de la firma (solo piezas_sueltas).
modoNo (default asincrono)asincrono o sincrono.
external_idNoSu propio identificador — útil para no firmar dos veces por un reintento.
202 Accepted Response (modo asíncrono, el default)
{
  "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:

POST Lo que les llega
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

GET Request
curl https://<su-instancia>/api/v1/firmas/3f1b... \
  -H "Authorization: Bearer <access_token>"
200 OK Response
{
  "job_id": "3f1b...",
  "status": "recibido" | "en_proceso" | "completado" | "error",
  "hash_sha256": null,
  "documento_url": null
}

Cuando status es completado, descargar:

GET Request
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.

POST Request
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]"
202 Accepted Response
{
  "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:

GET Request
curl https://<su-instancia>/api/v1/firmas/lote/<lote_id> \
  -H "Authorization: Bearer <access_token>"
200 OK Response
{
  "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ódigoSignificaQué hacer
400Falta un campo o el valor no es válido.Revisar el cuerpo (el mensaje dice qué falta).
401Token vencido o inválido.Pedir un token nuevo.
402Sin crédito suficiente.Avisarnos para cargar más saldo.
404El id no existe o no es de ustedes.Revisar el id.
409Todavía no terminó de firmarse.Reintentar la descarga más tarde.
410Se borró por retención (90 días).El registro sigue existiendo, el archivo no.
429Superaron el límite de peticiones.Esperar y reintentar con backoff.
502El 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ónLímite
Firmar (individual o lote)30 peticiones/minuto
Consultar estado / descargar120 peticiones/minuto
Pedir token10 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_secret guardado 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-Signature y el polling sigue funcionando igual.
  • Manejan 429 con backoff, no con reintento inmediato.
  • Ante 502 con PIN incorrecto, confirman con la persona autorizante antes de reintentar.
  • Usan external_id para 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.