ContratosVolver

Documentación

API y webhooks

Integra la firma de Contratos en tu CRM, ERP o portal: crea documentos desde un PDF o una plantilla, sigue su estado y descarga el PDF firmado con su certificado.

API y webhooksMCP para IAs

Contenido

  1. 1. Introducción
  2. 2. Autenticación
  3. 3. Modo prueba y planes
  4. 4. Primer documento en 1 minuto
  5. 5. Referencia de endpoints
  6. 6. Documentos
  7. 7. Firma en persona (iframe)
  8. 8. Plantillas
  9. 9. Cuenta, logo del QR y webs del iframe
  10. 10. Facturas
  11. 11. Informes
  12. 12. Webhooks
  13. 13. Errores
  14. 14. Colección de Postman

1. Introducción

La API es REST sobre HTTPS y responde en JSON, salvo las descargas, que devuelven el archivo (PDF, XML, ZIP o PNG). Todas las rutas cuelgan de:

url
https://contratos.compensador.es/api/v1

En la web hablamos de documentos. En la API se usa el término técnico envelope (sobre) en rutas, IDs y respuestas: un envelope es un proceso de firma con su PDF, sus firmantes y su auditoría.

Los estados de un documento son draft, sent, viewed, partiallySigned, completed, declined, voided (cancelado, por API o desde la web; avisa con el webhook envelope.voided) y expired (caducado: venció el plazo para firmar; la app lo marca cada 15 minutos y avisa con el webhook envelope.expired).

2. Autenticación

Cada petición lleva una clave API de tu organización como token Bearer. Las claves se crean en Configuración → Integraciones → Claves API y solo se muestran una vez, al crearlas. Puedes revocarlas en cualquier momento desde el mismo sitio.

http
Authorization: Bearer fo_xxx

Una clave solo ve y modifica los datos de su organización. Sin clave, o con una revocada, la respuesta es 401 No autorizado. Guárdala en el servidor, nunca en código que se ejecute en el navegador.

3. Modo prueba y planes

Para probar la integración sin gastar firmas, envía "isTest": true al crear un documento, o activa Configuración → Entorno → Modo prueba: con el modo activo, todo lo que se crea por API es de prueba.

  • Los documentos de prueba no consumen firmas ni créditos y se devuelven con isTest: true.
  • El PDF final y el certificado llevan la marca DOCUMENTO DE PRUEBA - SIN VALIDEZ (o CERTIFICADO DE PRUEBA - SIN VALIDEZ).
  • Sus webhooks incluyen test: true.

Con el plan gratuito, la API solo crea documentos de prueba (también desde plantilla) y no envía webhooks reales. Con cualquier plan de pago (o créditos) se crean documentos reales. Consulta lo que tienes disponible con GET /account.

4. Primer documento en 1 minuto

Sube un PDF y envíalo a un firmante. Con isTest=true no se consume ninguna firma:

bash
curl -X POST https://contratos.compensador.es/api/v1/envelopes \
  -H "Authorization: Bearer fo_xxx" \
  -F "subject=Contrato de servicios" \
  -F "document=@contrato.pdf;type=application/pdf" \
  -F 'recipients=[{"name":"Cliente Uno","email":"cliente@example.com"}]' \
  -F "isTest=true"

La respuesta trae el id del documento y el signingUrl del firmante. El firmante recibe además un email con el enlace. Después consulta el estado con GET /envelopes/{id} o, mejor, recibe el aviso por webhook cuando termine.

5. Referencia de endpoints

MétodoRutaQué hace
POST/envelopesCrea un documento subiendo un PDF
POST/envelopes/previewVista previa del PDF con los recuadros de firma (no crea nada)
GET/envelopesLista documentos (filtro por estado o por tu referencia, paginado)
GET/envelopes/{id}Detalle con firmantes, auditoría y accesos al enlace
GET/envelopes/{id}/documentPDF del documento (el firmado si ya existe)
GET/envelopes/{id}/certificateCertificado de firma en PDF
POST/envelopes/{id}/resendReenvía la invitación (y corrige el email)
POST/envelopes/{id}/signing-linkNuevo enlace de firma sin enviar email
POST/envelopes/{id}/cancelCancela el documento
GET/templatesLista plantillas con sus campos
POST/templatesCrea una plantilla o importa un fichero exportado
GET/templates/{id}Detalle de una plantilla con su HTML, CSS y schema
PUT/templates/{id}Cambia una plantilla (también archivar o reactivar)
GET/templates/{id}/exportExporta la plantilla en el formato de fichero de Contratos
POST/templates/{id}/envelopesCrea un documento desde una plantilla
GET/accountPlan, firmas disponibles y modo prueba
GET/organization/qr-logoLogo del QR (PNG)
PUT/organization/qr-logoSube o cambia el logo del QR
DELETE/organization/qr-logoQuita el logo del QR
GET/organization/embed-originsWebs que pueden mostrar la firma en un iframe
PUT/organization/embed-originsCambia esas webs
GET/invoicesLista tus facturas de Contratos
GET/invoices/{id}/pdfFactura en PDF
GET/invoices/{id}/xmlFactura en XML Facturae
GET/invoices/exportExportación ZIP o CSV de un periodo
GET/reports/summaryInforme de documentos de un periodo (JSON)
GET/reports/envelopes.csvDocumentos del informe en CSV

6. Documentos

Crear un documento desde un PDF

POST /api/v1/envelopes con multipart/form-data:

CampoDescripción
documentArchivo PDF. Obligatorio.
subjectAsunto del documento. Obligatorio.
messageMensaje para los firmantes. Opcional.
recipientsJSON con los firmantes: [{ name, email, signingOrder }]. Firman en el orden de signingOrder, uno detrás de otro. Obligatorio.
fieldsJSON con la posición de las firmas: [{ recipientEmail, recipientIndex, page, x, y, width, height, type }]. Opcional; el firmante sin campo recibe una firma en la página 1. Ver Campos de firma.
externalRefTu referencia del documento (hasta 200 caracteres), para buscarlo después. No tiene que ser única. Opcional.
metadataJSON con tus datos del documento, para reconocerlo al recibir un webhook. Opcional; ver Referencia y datos.
sendtrue (por defecto) envía el email de invitación al primer firmante.
embeddedtrue no envía email y devuelve un embedUrl para firmar dentro de un iframe de tu web. Solo con un firmante; ver Firma en persona.
isTesttrue crea un documento de prueba.
bash
curl -X POST https://contratos.compensador.es/api/v1/envelopes \
  -H "Authorization: Bearer fo_xxx" \
  -F "subject=Contrato" \
  -F "document=@contrato.pdf;type=application/pdf" \
  -F 'recipients=[{"name":"Cliente Uno","email":"cliente1@example.com","signingOrder":1},{"name":"Cliente Dos","email":"cliente2@example.com","signingOrder":2}]' \
  -F 'fields=[{"recipientEmail":"cliente1@example.com","page":1,"x":64,"y":680,"width":240,"height":72},{"recipientEmail":"cliente2@example.com","page":1,"x":315,"y":680,"width":240,"height":72}]' \
  -F "externalRef=contrato:2026-001" \
  -F 'metadata={"clienteId":"123","tipo":"contrato"}'

Respuesta 201:

json
{
  "id": "envelope_id",
  "documentId": "document_id",
  "status": "sent",
  "signingUrl": "https://contratos.compensador.es/sign/token",
  "isTest": false,
  "externalRef": "contrato:2026-001",
  "metadata": { "clienteId": "123", "tipo": "contrato" },
  "recipients": [
    { "id": "recipient_id", "name": "Cliente Uno", "email": "cliente1@example.com", "signingOrder": 1 },
    { "id": "recipient_id_2", "name": "Cliente Dos", "email": "cliente2@example.com", "signingOrder": 2 }
  ]
}

El signingUrl es el enlace del primer firmante y solo se devuelve aquí. Con embedded=true llega también como embedUrl. Si lo necesitas más tarde, pide uno nuevo. El PDF debe pesar menos de 25 MB; un PDF cifrado o dañado se rechaza con 400 El PDF no se puede leer.

Campos de firma

Las coordenadas son puntos PDF (1 pt = 1/72 de pulgada; un A4 mide 595 × 842) con origen en la esquina superior izquierda de la página:

  • page: página, desde 1. Tiene que existir en el PDF.
  • x: distancia del borde izquierdo de la página al borde izquierdo del recuadro.
  • y: distancia del borde superior de la página al borde superior del recuadro.
  • width y height: tamaño del recuadro, como mínimo 40 × 24 (recomendado 240 × 72). Tiene que caber entero en el tamaño real de la página.
  • type: signature (por defecto) o initials.

Cada campo va al firmante de su recipientEmail, que tiene prioridad, o al de su recipientIndex, que empieza en 0 (el primer firmante por signingOrder). Un campo sin ninguno de los dos va al firmante de su misma posición en la lista. El firmante sin campo recibe un recuadro por defecto en la página 1, ajustado al tamaño de la página.

Cambio: antes recipientIndex: 1 era el primer firmante; ahora es el segundo. Si ya enviabas fields, compruébalos con la vista previa.

Nada se corrige en silencio: un campo que no vale responde 400 con el motivo y su posición en fields (null si el problema es la lista entera):

json
{ "error": "fields[0]: la página 3 no existe (el PDF tiene 2)", "field": 0 }

Referencia y datos del integrador

  • externalRef: tu identificador del documento (hasta 200 caracteres) para buscarlo con GET /envelopes?externalRef=…. No tiene que ser único.
  • metadata: hasta 10 claves (de 1 a 40 caracteres: letras, números, _ . -) con valores de texto de hasta 500 caracteres; los números y booleanos se guardan como texto. En multipart va como texto JSON. Sirve para reconocer el documento en los webhooks; no se busca por ella.

Se envían al crear el documento, tanto subiendo un PDF como desde una plantilla; desde la web no se pueden poner, pero se ven en la ficha del documento y el buscador de Documentos encuentra por la referencia y por los valores de metadata. Los dos vuelven al crear, en el listado, en el detalle y en todos los webhooks del documento (null si no los enviaste).

Reintentos sin duplicar

Si la conexión se corta al crear un documento, no sabes si se creó. Para reintentar sin crear otro, manda la cabecera Idempotency-Key con una clave única por documento (un UUID, por ejemplo). Vale al subir un PDF y al crear desde una plantilla.

http
POST /api/v1/envelopes
Authorization: Bearer fo_xxx
Idempotency-Key: 6f1c2a9e-0d4b-4c1e-9a77-1b2c3d4e5f60
  • Con la misma clave y los mismos datos durante 24 horas no se crea nada: recibes la respuesta de la primera (el mismo id y el mismo signingUrl) con la cabecera Idempotent-Replayed: true, sin otro email ni otra firma gastada.
  • 409 si la primera aún está en curso (reintenta en unos segundos) y 422 si la clave ya se usó con otros datos (usa otra).
  • Si la primera terminó con error, la clave no queda gastada: corrige y reintenta con la misma.

externalRef no sirve para esto porque no tiene que ser única (un contrato rechazado se puede volver a crear con la misma referencia), pero puedes usarla como base de la clave.

Vista previa

Para ver dónde caerá la firma antes de enviar. Mismo multipart que la creación (document, recipients y fields); no guarda nada, no envía nada, no gasta firmas y funciona también en el plan gratuito.

bash
curl -X POST https://contratos.compensador.es/api/v1/envelopes/preview \
  -H "Authorization: Bearer fo_xxx" \
  -F "document=@contrato.pdf;type=application/pdf" \
  -F 'recipients=[{"name":"Cliente Uno","email":"cliente1@example.com"}]' \
  -F 'fields=[{"recipientIndex":0,"page":2,"x":315,"y":604,"width":240,"height":72}]' \
  -o vista-previa.pdf

Devuelve el PDF con cada recuadro dibujado en verde y rotulado «Firma - nombre». recipients es opcional: sin él, cada campo se dibuja tal cual, en su orden, rotulado «Firmante 1», «Firmante 2»… Con ?format=json devuelve los campos tal como se usarían al crear:

json
{
  "pageCount": 2,
  "pages": [{ "width": 595.28, "height": 841.89 }, { "width": 595.28, "height": 841.89 }],
  "fields": [
    { "recipientIndex": 0, "recipientEmail": "cliente1@example.com", "type": "signature", "page": 2, "x": 315, "y": 604, "width": 240, "height": 72 }
  ]
}

Los errores son los mismos 400 que al crear.

Listar documentos

http
GET /api/v1/envelopes?status=sent&take=25&cursor=envelope_id
GET /api/v1/envelopes?externalRef=contrato:2026-001

status es opcional y take va de 1 a 100 (25 por defecto). externalRef filtra por tu referencia exacta y puede devolver varios documentos, del más reciente al más antiguo. La respuesta trae data y nextCursor: pásalo como cursor para pedir la página siguiente. Cuando es null, no hay más.

Detalle de un documento

http
GET /api/v1/envelopes/{id}

Devuelve el documento con su externalRef y su metadata, sus firmantes (en orden de firma), los campos, las firmas, la fecha de cierre (finishedAt) y la auditoría con la IP y el navegador de cada evento. Si está cancelado (voided), voidedAt es cuándo y voidReason el motivo (null si no se indicó); en el resto de estados, los dos valen null.

Incluye también linkAccesses: cada vez que un firmante abrió su enlace, con la fecha, la IP, el navegador y si fue la primera apertura (firstOpen). Varias recargas seguidas desde la misma IP en menos de 5 minutos cuentan como una.

Descargar el PDF y el certificado

http
GET /api/v1/envelopes/{id}/document
GET /api/v1/envelopes/{id}/certificate

Devuelven application/pdf. El documento es el PDF firmado si el proceso ha terminado y el original si no. El certificado responde 202 mientras el proceso no haya cerrado. Añade ?download=1 para recibirlo como descarga.

Reenviar la invitación

http
POST /api/v1/envelopes/{id}/resend
Content-Type: application/json

{ "email": "email.corregido@example.com" }

Genera un enlace nuevo para el firmante al que le toca firmar, invalida el anterior y le envía el email. El cuerpo es opcional: con email, corrige antes su dirección. El cambio queda en la auditoría.

json
{
  "id": "envelope_id",
  "status": "sent",
  "recipientId": "recipient_id",
  "recipientEmail": "email.corregido@example.com",
  "emailChanged": true,
  "signingUrl": "https://contratos.compensador.es/sign/token",
  "expiresAt": "2026-10-27T10:00:00.000Z"
}

signingUrl es el enlace nuevo y expiresAt su caducidad (7 días), por si quieres mandarlo también por tu cuenta. Un borrador creado con send=false pasa a enviado. Un documento caducado se reabre (vuelve a sent, o a partiallySigned si ya firmó alguien).

Nuevo enlace de firma

http
POST /api/v1/envelopes/{id}/signing-link

Invalida el enlace del firmante al que le toca firmar y devuelve uno nuevo de 7 días, sin enviar email: para volver a abrir el iframe si el cliente lo cerró sin firmar, o para mandar el enlace por tu cuenta. El documento vuelve a caducar a los 7 días; si ya estaba caducado, se reabre.

json
{
  "signingUrl": "https://contratos.compensador.es/sign/token",
  "embedUrl": "https://contratos.compensador.es/sign/token",
  "expiresAt": "2026-10-26T10:00:00.000Z",
  "recipient": { "id": "recipient_id", "name": "Cliente Uno" }
}

Responde 409 si el documento ya está cerrado (completado, rechazado o cancelado), es un borrador (creado con send=false) o no queda ningún firmante pendiente.

Cancelar

http
POST /api/v1/envelopes/{id}/cancel
Content-Type: application/json

{ "reason": "El cliente ha pedido cambios en el contrato" }

Pasa el documento a voided (cancelado), invalida todos sus enlaces de firma y avisa con el webhook envelope.voided. Se puede cancelar cualquier documento que no esté cerrado, también un borrador. El cuerpo es opcional: reason (texto, hasta 500 caracteres) es el motivo, que queda en la auditoría y llega en el webhook; sin cuerpo, o con reason vacío, no hay motivo y vale null. Si hay cuerpo, se lee como JSON sea cual sea el Content-Type (también con curl -d).

json
{
  "id": "envelope_id",
  "status": "voided",
  "voidedAt": "2026-10-21T10:00:00.000Z",
  "reason": "El cliente ha pedido cambios en el contrato"
}

Errores: 400 si el cuerpo no es un objeto JSON, pasa de 10 KB, reason no es texto o pasa de 500 caracteres; 401 sin clave API válida; 404 si el documento no existe o es de otra organización; 409 si ya está cerrado (completado, rechazado, cancelado o caducado). También se puede cancelar desde la web, en el detalle del documento. Después, el detalle (GET /api/v1/envelopes/{id}) devuelve la fecha y el motivo en voidedAt y voidReason.

7. Firma en persona (iframe)

Para que alguien firme en el momento dentro de tu web o tu app (por ejemplo, en una tablet en tu oficina), crea el documento con embedded=true y abre el embedUrl de la respuesta en un iframe. No se envía ningún email.

  • Permite tu web antes. En Configuración → Integraciones → Webs que pueden mostrar la firma, o por API (abajo), añade el origen de la página que contiene el iframe (por ejemplo https://portal.tuempresa.com). Sin ninguna web permitida, el navegador no carga el iframe.
  • Un solo firmante. Con embedded=true y más de un firmante, la API responde 400. Si firman varias personas, envía el documento por email.
  • Aviso al terminar. Cuando el firmante firma o rechaza, la página del iframe envía un mensaje a tu web con postMessage:
html
<iframe src="EMBED_URL" style="width:100%;height:90vh;border:0" allow="clipboard-write"></iframe>
<script>
  window.addEventListener("message", (event) => {
    if (event.origin !== "https://contratos.compensador.es") return;
    if (event.data?.type !== "contratos:signing") return;
    // event.data.status: "signed" | "declined"
    console.log(event.data.status, event.data.envelopeId, event.data.recipientId);
  });
</script>

Webs permitidas por API

bash
curl -X PUT https://contratos.compensador.es/api/v1/organization/embed-origins \
  -H "Authorization: Bearer fo_xxx" \
  -H "Content-Type: application/json" \
  -d '{"origins":["https://portal.tuempresa.com"]}'

Sustituye la lista entera y devuelve { "origins": [...] } ya normalizada. Usa https:// (solo http://localhost para pruebas), sin rutas ni comodines, hasta 10 webs; [] la vacía. Si alguna no vale, responde 422 con el motivo. GET devuelve la lista actual, que también aparece en GET /account como embedAllowedOrigins.

Aviso al terminar

Comprueba siempre event.origin. Para guardar el resultado en tu sistema, confía en los webhooks signature.completed y signature.declined, o consulta el documento por API; el mensaje sirve para cerrar el iframe y seguir.

Ten en cuenta que en la firma en persona la IP y el dispositivo que quedan en las evidencias son los de tu equipo, no los del firmante.

8. Plantillas

Las plantillas son HTML/CSS con variables y un schema con los datos que piden. Se diseñan en la web o por API (ver Crear, editar, exportar e importar). Para usar una, primero consulta qué datos pide:

http
GET /api/v1/templates
GET /api/v1/templates/{id}

Por defecto se listan solo las plantillas activas; con ?archived=include salen también las archivadas, que no se pueden usar. Cada plantilla trae fields, en orden: key (el nombre que hay que enviar), type (string, number, date, email o dni), label, placeholder, default y, si tiene formato, pattern y patternMessage. Todos los campos son obligatorios.

json
{
  "data": [
    {
      "id": "template_id",
      "name": "Reclamación de vuelo",
      "active": true,
      "fields": [
        { "key": "nombre_completo", "type": "string", "label": "Nombre completo", "required": true },
        { "key": "dni", "type": "dni", "label": "DNI", "required": true },
        { "key": "fecha_firma", "type": "date", "label": "Fecha de firma", "default": "2026-10-16", "required": true }
      ]
    }
  ]
}

Después crea el documento enviando esos datos en data:

http
POST /api/v1/templates/{id}/envelopes
Content-Type: application/json

{
  "subject": "Firma de reclamación de vuelo",
  "message": "Revisa y firma este documento.",
  "data": {
    "nombre_completo": "María Pérez",
    "dni": "12345678Z",
    "fecha_firma": "2026-10-16"
  },
  "recipients": [
    { "name": "María Pérez", "email": "maria@example.com", "signingOrder": 1 }
  ],
  "send": true,
  "isTest": true,
  "externalRef": "presupuesto:2026-0042",
  "metadata": { "presupuestoId": "42" }
}

Contratos valida los datos (el DNI/NIE con su letra de control, las fechas, los formatos) y responde 400 con el motivo si algo no cuadra. La posición de las firmas la define la propia plantilla. externalRef y metadata son opcionales y funcionan igual que al subir un PDF (ver Referencia y datos). Admite también embedded para el iframe y la cabecera Idempotency-Key (ver Reintentos sin duplicar).

Crear, editar, exportar e importar

El detalle (GET /templates/{id}) devuelve además el contenido: html, css y schema (o null). Para crear una plantilla:

http
POST /api/v1/templates
Content-Type: application/json

{
  "name": "Presupuesto",
  "html": "<h1>Presupuesto para {{cliente}}</h1><div data-signature-field='0' style='width:240px;height:72px'></div>",
  "css": "h1 { color: #047857; }",
  "schema": {
    "fields": { "cliente": { "type": "string", "label": "Cliente" } },
    "fields_order": ["cliente"]
  }
}

name y html son obligatorios; css y schema (un objeto) son opcionales. Límites: nombre de 200 caracteres, HTML de 2 MB, CSS de 512 KB, schema de 256 KB y cuerpo de 12 MB. Responde 201 con el detalle. El PDF se genera sin JavaScript y solo carga imágenes, fuentes y estilos de webs públicas.

Para cambiarla, PUT /api/v1/templates/{id} con solo lo que cambia: name, html, css, schema (null lo quita) o active (false archiva y true reactiva). Los documentos ya creados no cambian.

http
PUT /api/v1/templates/{id}
Content-Type: application/json

{ "name": "Presupuesto 2027", "active": false }

Para llevarla a otra cuenta, expórtala y envía el resultado tal cual a POST /api/v1/templates con la clave de la otra organización. Es el mismo fichero que descarga el botón «Exportar» de la web.

bash
curl https://contratos.compensador.es/api/v1/templates/{id}/export \
  -H "Authorization: Bearer fo_origen" -o presupuesto.json

curl -X POST https://contratos.compensador.es/api/v1/templates \
  -H "Authorization: Bearer fo_destino" \
  -H "Content-Type: application/json" \
  --data-binary @presupuesto.json

Importar siempre crea una plantilla nueva: si ya hay una con el mismo nombre, la nueva se llama «Presupuesto (importada)». Errores: 400 si el cuerpo no es un objeto JSON, 404 si la plantilla no es de tu organización, 413 si pasa de 12 MB y 422 con el motivo si los datos no son válidos.

9. Cuenta, logo del QR y webs del iframe

http
GET /api/v1/account

Devuelve si la organización está en modo prueba, su plan, las firmas que le quedan (planRemaining y creditsBalance), cuándo caducan sus próximos créditos (creditsNextExpiry, fecha ISO o null) y cuántos (creditsExpiringNext), si puede crear documentos reales (canCreateRealDocuments), las webs permitidas para el iframe (embedAllowedOrigins) y si su plan permite documentos reales por API (apiTemplatesAllowed es siempre true: las plantillas están en todos los planes). Úsalo antes de crear documentos para avisar al usuario si se ha quedado sin firmas. Con unlimited: true la organización no tiene cobro (uso interno o plataforma sin cobro): firma sin límite y planRemaining es null. Los créditos prepago tienen una validez de 1 año desde la compra y se gastan primero los que caducan antes.

El QR del enlace de firma puede llevar un logo pequeño en el centro. Es lo único de la marca que se cambia por API; colores y logo grande siguen en Configuración → Branding. Las webs que pueden mostrar la firma en un iframe se cambian con PUT /organization/embed-origins.

bash
curl -X PUT https://contratos.compensador.es/api/v1/organization/qr-logo \
  -H "Authorization: Bearer fo_xxx" \
  -F "logo=@logo.png;type=image/png"

Acepta PNG o JPG de hasta 300 KB: como archivo en el campo logo, como JSON { "imageBase64": "..." } o con la imagen directamente en el cuerpo. GET devuelve el logo guardado (o 404) y DELETE lo quita.

10. Facturas

Son las facturas que Contratos emite a tu organización por el plan y los créditos, para que tu contabilidad las recoja sola.

http
GET /api/v1/invoices?from=2026-01-01&to=2026-03-31&take=100
GET /api/v1/invoices/{id}/pdf
GET /api/v1/invoices/{id}/xml
GET /api/v1/invoices/export?from=2026-01-01&to=2026-03-31
GET /api/v1/invoices/export?from=2026-01-01&to=2026-03-31&format=csv

El listado admite from/to (AAAA-MM-DD), status, take y cursor, y devuelve número, fechas, importes en céntimos, comprador y rutas de descarga. La exportación devuelve un ZIP con los PDF, los XML y un CSV contable, o solo el CSV con format=csv (máximo 1000 facturas por exportación).

Las facturas de prueba (serie TEST-, sin validez fiscal) llevan isTest: true. Con el parámetro test eliges include, exclude u only; la exportación las excluye por defecto.

11. Informes

Las cifras de la sección Informes de la web, para llevarlas a tu cuadro de mando: documentos enviados en un periodo, cómo terminaron, tasa de firma, tiempo hasta la firma y firmas gastadas, comparados con el periodo anterior.

bash
curl "https://contratos.compensador.es/api/v1/reports/summary?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer fo_xxx"
ParámetroDescripción
from / toDías AAAA-MM-DD en hora de Madrid, to incluido. Obligatorios; máximo 366 días.
groupByday, week o month para la serie por intervalo. Por defecto según la duración: hasta 31 días por día, hasta 120 por semana y, si no, por mes.
sourceall (por defecto), uploaded (PDF subido en la web), template (plantilla en la web) o api.
templateIdSolo los documentos de esa plantilla de tu organización.
includeTesttrue para incluir los documentos de prueba. Por defecto false.
json
{
  "range": { "from": "2026-09-01", "to": "2026-09-30", "groupBy": "day" },
  "previousRange": { "from": "2026-08-01", "to": "2026-08-31", "partial": false },
  "filters": { "source": "all", "templateId": null, "includeTest": false },
  "totals": {
    "sent": 45, "completed": 34, "pending": 5, "expired": 3, "declined": 2, "voided": 1,
    "signRate": 0.85, "medianHoursToSign": 5.3, "signaturesUsed": 36, "openNow": 9
  },
  "previous": { "sent": 36, "completed": 28, "signRate": 0.8, … },
  "variations": { "sent": { "kind": "pct", "value": 25 }, "signRate": { "kind": "pct", "value": 6 }, … },
  "series": { "byPeriod": [ … ], "signTime": { … }, "bySource": { … }, "topTemplates": { … } },
  "allowance": { "planIncluded": 50, "planUsed": 36, "creditsBalance": 20 }
}
  • Cada documento cuenta en el periodo de su primer envío, con su resultado efectivo: uno abierto cuyo enlace ya venció cuenta como caducado (expired) aunque aún no se haya marcado.
  • signRate es completados / terminados como fracción de 0 a 1 (0.85 = 85 %); medianHoursToSign, la mediana de horas desde el envío hasta la firma. Los dos son null si no hay datos.
  • signaturesUsed son los documentos cuya firma se cobró en el periodo; openNow, los abiertos sin caducar ahora mismo (sin filtro de periodo).
  • Se compara con el mes, trimestre o año natural anterior si el periodo es uno completo; si no, con los mismos días justo antes. Si el periodo está en curso (to posterior a hoy), solo se compara el tramo transcurrido: del 1 al 20 de octubre frente al 1-20 de septiembre (hasta fin de mes si ese día no existe), con previousRange.partial: true. variations da { kind: "pct", value } en %, { kind: "new" } si antes era 0 o { kind: "none" } si los dos son 0.
  • allowance son las firmas del plan en el ciclo actual y los créditos de hoy.

GET /api/v1/reports/envelopes.csv con los mismos parámetros descarga un documento por fila (UTF-8 con BOM y ;, para Excel), con las columnas id, asunto, estado, origen, plantilla, enviado, terminado, horas_hasta_firma, firmantes, firmas_cobradas, prueba, externalRef. Los textos que empiezan por =, +, - o @ llevan un ' delante para que Excel no los ejecute como fórmulas. Admite hasta 10.000 documentos; si hay más, responde 400.

Las dos rutas responden 400 si faltan las fechas o no son válidas, si el periodo supera 366 días o si templateId no es una plantilla de tu organización.

12. Webhooks

En lugar de consultar el estado cada poco, Contratos puede avisar a tu sistema. Configura la URL en Configuración → Integraciones → Webhooks y recibirás un POST JSON en cada evento:

EventoCuándo
envelope.sentSe envía el documento, o le llega el turno al siguiente firmante
signing.viewedUn firmante abre el documento por primera vez
signature.completedUn firmante firma
signature.declinedUn firmante rechaza firmar
envelope.completedHan firmado todos: el PDF final y el certificado están listos
envelope.declinedEl proceso termina con algún rechazo
envelope.expiredVence el plazo para firmar sin que hayan firmado todos
envelope.voidedSe cancela el documento (por API o desde la web), con el motivo en reason (null si no se indicó) y la fecha en voidedAt
webhook.testPulsas «Enviar webhook de prueba» en Configuración
json
{
  "id": "6f1c…",
  "eventType": "envelope.completed",
  "createdAt": "2026-10-16T10:24:00.000Z",
  "data": {
    "externalRef": "contrato:2026-001",
    "metadata": { "clienteId": "123", "tipo": "contrato" },
    "documentUrl": "https://contratos.compensador.es/api/v1/envelopes/envelope_id/document?download=1",
    "certificateUrl": "https://contratos.compensador.es/api/v1/envelopes/envelope_id/certificate?download=1",
    "envelopeId": "envelope_id",
    "subject": "Contrato de servicios",
    "test": false,
    "status": "completed",
    "finalHash": "sha256…",
    "certificateAvailable": true
  }
}

Todos los eventos de un documento llevan su externalRef y su metadata (null si no tiene). envelope.completed y envelope.declined añaden documentUrl y certificateUrl: descárgalos con tu clave API (el certificado puede responder 202 unos instantes).

Cada petición lleva las cabeceras X-Contratos-Event, X-Contratos-Webhook-Id, X-Contratos-Delivery-Attempt y X-Contratos-Signature: la firma HMAC SHA-256 en hexadecimal del cuerpo exacto, con el secreto whsec_… que ves al configurar el webhook. Compruébala siempre antes de fiarte del aviso:

js
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: el cuerpo tal cual llega, sin parsear
function verifyContratosWebhook(rawBody, signature, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return signature.length === expected.length
    && timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
}

Responde con un código 2xx. Si tu servidor falla o no contesta, Contratos reintenta hasta 3 veces en unos segundos. Puedes ver cada entrega y su resultado en Configuración → Integraciones.

13. Errores

Los errores responden JSON con un campo error que explica el motivo en español:

json
{ "error": "No hay firmas disponibles. Activa una suscripción o agrega créditos." }
CódigoSignificado
400Faltan datos o no son válidos. Si el problema es un campo de firma, la respuesta trae también field con su posición en fields
401Sin clave API, clave revocada o MCP desactivado
402No quedan firmas en el plan ni créditos
403El plan no permite la operación (por ejemplo, documentos reales con el plan gratuito)
404No existe, o es de otra organización
409El documento ya está cerrado, es un borrador o no queda firmante pendiente (nuevo enlace de firma); o hay una petición en curso con la misma Idempotency-Key
413 / 422Archivo demasiado grande o imagen no válida; 422 también si la Idempotency-Key ya se usó con otros datos

14. Colección de Postman

Todas las peticiones de esta página, listas para probar. Impórtala en Postman y rellena las variables baseUrl (https://contratos.compensador.es) y apiKey.

Descargar colección de Postman

¿Prefieres que una IA lo haga por ti? Mira la guía del MCP para IAs.

Contratos

Firma electrónica online para empresas.

Un servicio deCompensador Servicios Jurídicos
API y MCPTérminos de servicioProtección de datosEjercer derechos RGPDCookies