La API de Docus
Conecta el sistema de tu empresa con Docus para crear contratos desde tus plantillas, mandarlos a firma y enterarte de lo que pasa con cada uno. Es una API REST con JSON, de servidor a servidor.
- Dirección base:
https://www.docusapp.com/api/v1 - Especificación completa (OpenAPI 3.1), para Postman o Swagger: https://www.docusapp.com/api/v1/openapi.json
Autenticación y permisos
Cada pedido lleva Authorization: Bearer dcs_live_…. La clave la crea, en Configuración → API, quien administra las integraciones de tu empresa, y se muestra una sola vez. La API tiene que estar activa en tu cuenta: la activa el equipo de Docus.
El ambiente de prueba está en preparación y todavía no está disponible. Cuando lo esté, tendrá sus propias claves (dcs_test_…), nada de lo que se firme ahí tendrá validez legal, y los correos de la firma quedarán en una bandeja (la lees en Configuración → API o con GET /test/emails, una ruta que solo existirá en ese ambiente) y saldrán por correo solo a quien sea miembro de tu organización. Mientras tanto, GET /me ya dice en qué ambiente estás (environment, hoy siempre live) y cada aviso lo repite (livemode, hoy siempre true).
Una clave actúa como la cuenta de integración de tu empresa, no como una persona: si quien la creó deja el equipo, la clave sigue funcionando, y lo que hace queda registrado a nombre de la integración. Cada clave tiene los permisos que le diste:
| Permiso | Casilla en Configuración → API | Qué deja hacer |
|---|---|---|
templates:read | «Leer plantillas» (viene marcada) | Listar las plantillas y ver qué datos pide cada una. |
contracts:read | «Leer contratos» (viene marcada) | Listar los contratos, ver su estado y sus firmantes, bajar el PDF firmado y su historial. |
contracts:write | «Crear contratos» | Crear borradores desde una plantilla, con sus datos y sus firmantes. |
signatures:send | «Mandar a firma» | Enviar un contrato a firmar, recordar a quien no ha firmado y detener la firma. |
Prueba la conexión con GET /me: devuelve tu empresa y los permisos de la clave.
Límites, errores e idempotencia
- Hasta 120 pedidos por minuto por clave; las respuestas traen las cabeceras
RateLimit-*y, si te pasas, un 429 conRetry-After. - Todo error es
{ "error": { "code", "message", "reason"? } }: elcodees estable, elmessageestá en español para quien lo lee en un registro y, cuando uncodeagrupa varios motivos,reasondice cuál. Lo traensend_blocked,wrong_statusytemporarily_unavailable, y también algunosinvalid_request(por ejemplotoo_many_signersounfilled_signature_slots, al crear un contrato). Toda respuesta traeX-Request-Id: inclúyelo si nos escribes. - En los POST, manda
Idempotency-Key: si reintentas con la misma llave recibes la misma respuesta sin repetir la operación (vale 24 horas). Y para no crear dos veces el mismo contrato pase lo que pase, manda suexternal_ref, tu identificador, que es único en tu cuenta. - Con la llave queda guardada la respuesta del pedido salvo las 5xx y la 429, también los errores 4xx (el 400 por el cuerpo, 404, 409, 422…): si un pedido falló y lo corriges, mándalo con una llave nueva, o recibirás otra vez la respuesta del pedido malo (o
422 idempotency_key_reused, porque el pedido corregido ya es otro). Las 5xx y la 429 no quedan guardadas: ahí sí reintentas con la misma. Tampoco quedan guardadas las que se contestan antes de reservar la llave: los 401 y los 403 de la clave (insufficient_scope,module_disabled,integration_unavailable) y el 400 de unaIdempotency-Keymal formada. Un 403forbiddenque sale ya dentro del pedido (el rol de la cuenta de integración no alcanza) sí queda guardado.
Crear un contrato y mandarlo a firma
Son dos pasos, con dos permisos:
POST /contractscrea un borrador desde una plantilla, con sus datos y sus firmantes. Qué datos pide cada plantilla lo diceGET /templates/{id}.POST /contracts/{id}/sendlo manda a firma, pasando por los mismos controles que en Docus (aprobaciones, revisión obligatoria, plazo, firmantes). Si algo lo frena, respondesend_blockedcon el motivo enreason.
Los datos van en data, con las claves de fields y de date_inputs de GET /templates/{id}. Esas claves son las variables que la plantilla escribe como {{clave}} en su texto: la key es el nombre que va entre llaves. Una clave que la plantilla no pide, o que calcula Docus (computed), responde 400 invalid_request y nombra las claves. Los campos de lista (select y select_other) traen sus options: manda el value.
La API no firma por nadie. Cada persona firma desde el enlace que le llega por correo, con un código que recibe ahí: es la firma electrónica simple de la Ley 19.799. Con POST /contracts/{id}/remind se le recuerda a quien no ha firmado, y con POST /contracts/{id}/stop se detiene la firma.
Firma en turnos: a quién le toca
Una plantilla puede tener varios espacios de firma (cada línea de firma con su rol debajo, como «Representante legal» o «Aval»). GET /templates/{id} los lista, en orden, en signature_slots: [{ "slot": 1, "role": "Representante legal" }, …].
Al crear el contrato, cada firmante dice en qué espacio firma, con su role o con su slot, y con signing_mode eliges si firman en turnos (sequential: primero el espacio 1, y cada uno espera a que firme el anterior) o todos a la vez:
POST /contracts
{
"template_id": "…",
"signing_mode": "sequential",
"data": { … },
"signers": [
{ "role": "Representante legal", "name": "Ana Rojas", "email": "ana@empresa.cl" },
{ "role": "Aval", "name": "Luis Soto", "email": "luis@aval.cl" },
{ "role": "Contraparte", "name": "Marta Díaz", "email": "marta@contraparte.cl" }
]
}- Si un firmante trae
roleoslot, todos lo traen; mezclar responde400. Ysigning_modees obligatorio. - Los firmantes cubren todos los espacios, una vez cada uno. Si falta alguno, o uno se repite, o un rol no existe, responde
400 invalid_requesty el mensaje nombra los espacios. - El
rolese compara sin tildes ni mayúsculas. Una plantilla que repite un rol (dos «Testigo») solo admiteslot(conroleresponde400). Si el rol de la plantilla trae datos entre llaves ({{comprador}}), te recomendamosslot, la llave estable de un espacio: elrolese compara con el texto de la plantilla tal cual, llaves incluidas, y en el contrato queda con los datos puestos. - Sin
rolenislot, el firmante N ocupa el espacio N, como siempre. - Un contrato sin firmantes, o con algún espacio de firma sin nadie, no sale a firma por la API:
POST /contracts/{id}/sendresponde422 send_blockedconreason: no_signerssi no tiene ninguno, y conreason: unfilled_signature_slotssi falta cubrir un espacio (el mensaje dice cuáles). Los dos se revisan antes de las reglas de aprobación (no crean ninguna solicitud), y sin firmantes respondeno_signersantes que los frenos de firmas ubicadas, reglas de aprobación, plazo y revisión obligatoria; solo van antes los permisos, el estado de borrador, las propuestas o aprobaciones pendientes y los datos sin completar. Un PDF subido tal cual no se mide por los espacios (el cero firmantes sí).
A quién le toca: GET /contracts/{id} trae el slot y el role de cada firmante y un progress. El role es el que el sellado calcula para el bloque de firmas de ese contrato: el texto de la línea con los datos ya puestos (puede diferir del de la plantilla si ella lo escribía con datos entre llaves); si el documento no trae el de su espacio, el de su posición entre los firmantes y, si tampoco, «Firmante». Es el mismo que usa el PDF firmado, salvo que dos firmantes compartan un espacio (la API los ordena por id); y con las firmas ubicadas a mano sobre el PDF el sellado no escribe rol, aunque el campo igual se informa. Para identificar un espacio usa el slot (y el next_signer_id), no el texto del rol.
GET /contracts/{id}
{
"status": "pending_signature",
"signing_mode": "sequential",
"signers": [
{ "id": "…", "slot": 1, "role": "Representante legal", "status": "signed", … },
{ "id": "…", "slot": 2, "role": "Aval", "status": "notified", … },
{ "id": "…", "slot": 3, "role": "Contraparte", "status": "pending", … }
],
"progress": {
"signed": 1,
"total": 3,
"next_signer_id": "<el id del Aval>",
"awaiting_slot": 2,
"awaiting_role": "Aval"
}
}next_signer_id,awaiting_slotyawaiting_roledicen a quién le toca según el orden de firma, no a quién ya le llegó su enlace: elstatusde ese firmante dice si Docus ya le mandó, o intentó mandarle, su enlace (notified) o todavía no (pending). Si el correo no llegó,remindlo reintenta. Solo hay turno si el contrato está en firma (pending_signature) y essequential; en paralelo, o en cualquier otro estado, sonnull. También sonnullsi el firmante a quien le toca rechazó: nadie más puede firmar hasta que se detenga la firma o se le reenvíe.- Ante cualquier aviso, pide el contrato y decide por
progress, no por el orden en que llegaron los avisos: pueden llegar desordenados (contract.completedpuede llegar antes que elsigner.signeddel último). El aviso dice que algo cambió; el contrato dice cómo quedó. expiredno es final: quien administra el contrato en Docus puede elegir una fecha nueva y reactivar la firma, y los firmantes pueden seguir firmando sin que llegue un aviso de reactivación. Mientras te interese el contrato, sigue consultándolo.- El PDF sellado (
GET /contracts/{id}/pdf) existe solo cuando el contrato estásigned; antes responde404.
Avisos a tu sistema (webhooks)
En Configuración → API registras la dirección https de tu sistema y los eventos que quieres recibir. Cuando pasa uno, Docus le manda un POST con el evento en JSON.
| Evento | Cuándo llega |
|---|---|
contract.created | Se creó un contrato en tu cuenta, por la API o desde Docus. Una autofirma también: llegan contract.created, signer.signed y contract.completed, sin contract.sent. |
contract.sent | El contrato pasó de borrador a «en firma». |
contract.completed | Firmaron todos y Docus selló el PDF. Ya se puede bajar con GET /contracts/{id}/pdf. |
contract.expired | Pasó el plazo para firmar sin que firmaran todos. |
contract.cancelled | El contrato se canceló (por ejemplo, al rechazarse su aprobación). |
contract.signing_stopped | Alguien detuvo la firma: el contrato volvió a borrador y los enlaces dejaron de servir. |
signer.notified | A un firmante le tocó firmar y Docus le mandó (o intentó mandarle) su enlace; en firma secuencial, cuando le llega el turno. Si el correo no salió, POST /contracts/{id}/send lo dice en emails_failed y remind lo reintenta. |
signer.viewed | La página de firma de un firmante se abrió en un navegador por primera vez en este envío. Dice que se abrió, no quién la abrió ni que alguien leyó el documento: es una señal, no una prueba. |
signer.signed | Un firmante firmó con su código. Aunque sea el último, el contrato sigue «en firma» hasta que Docus lo sella: «firmado por todos» es contract.completed. |
signer.declined | Un firmante rechazó firmar. El contrato sigue «en firma» hasta que alguien lo detenga o venza. |
El cuerpo es delgado a propósito: ids, estados y tu external_ref, sin nombres, correos ni RUT. El detalle se pide con GET /contracts/{id}, con tu clave. Un aviso se ve así:
{
"id": "7d1f0e52-3c4b-4a8e-9f61-2b5c8d0a1e93",
"type": "signer.signed",
"created_at": "2026-10-04T15:21:08.412901+00:00",
"livemode": false,
"data": {
"contract": {
"id": "33be1e00-1111-4222-8333-444455556666",
"code": "DOC-33BE1E",
"status": "pending_signature",
"external_ref": "evento-1042"
},
"signer": {
"id": "a1c2e3f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
"status": "signed",
"external_ref": "staff-88"
}
}
}Sobre signer.viewed: dice que la página de firma se abrió en un navegador, no quién la abrió ni que alguien leyó el documento. Se registra una vez por envío, cuando la página terminó de cargar y está a la vista. Los programas que solo revisan los enlaces de un correo no la cargan y no cuentan, y tampoco cuenta quien la abre con una sesión de Docus de otro correo; pero un correo reenviado, o un revisor de enlaces que carga la página entera, sí contaría: tómalo como una señal, no como una prueba. Si se detiene la firma y se vuelve a mandar, o se le reenvía a alguien que rechazó, cuenta de nuevo. El detalle del contrato trae viewed_at y last_viewed_at de cada firmante.
Verificar que el aviso es de Docus
Cada aviso trae Docus-Signature: t=…,v1=…. t es la hora en segundos Unix y v1 es un HMAC-SHA256, en hexadecimal, con el secreto whsec_… de esa dirección, sobre el texto t + "." + cuerpo. Calcúlalo con el cuerpo tal cual llegó (antes de interpretar el JSON) y rechaza los de más de 5 minutos. En Node:
const crypto = require("node:crypto");
// secreto: el whsec_… de la dirección (Configuración → API).
// cuerpoCrudo: el cuerpo tal cual llegó, como texto (antes de JSON.parse).
// cabecera: el valor de Docus-Signature.
function avisoDeDocusEsValido(secreto, cuerpoCrudo, cabecera) {
if (!cabecera) return false;
let t = null;
const firmas = [];
for (const parte of cabecera.split(",")) {
const [clave, valor] = parte.trim().split("=");
if (clave === "t") t = Number(valor);
if (clave === "v1") firmas.push(valor);
}
if (!t || firmas.length === 0) return false;
// Rechaza avisos de más de 5 minutos: así uno capturado no se puede reenviar.
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const esperada = crypto
.createHmac("sha256", secreto)
.update(t + "." + cuerpoCrudo)
.digest();
return firmas.some((f) => {
const recibida = Buffer.from(f, "hex");
return recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada);
});
}El secreto se muestra al registrar la dirección. Si lo pierdes o se filtra, «Renovar secreto» crea otro y el anterior deja de valer en ese momento.
Cómo llegan
- Responde con cualquier código 2xx en menos de 10 segundos. Si no, Docus reintenta a los 1 min, 5 min, 30 min, 2 horas, 6 horas, 12 horas, 24 horas: 8 intentos en total. Los últimos 30 avisos de tu cuenta se ven en Configuración → API, y desde ahí se pueden reenviar; lo anterior se recupera con
GET /events. - La entrega es al menos una vez: el mismo evento puede llegar dos veces. Deduplica por su
id(también viene en la cabeceraDocus-Event-Id; además lleganDocus-Event-Type,Docus-Delivery-IdyDocus-Delivery-Attempt). - El orden en que llegan no está garantizado, y varios eventos del mismo cambio (los
signer.notifiedde un envío) compartencreated_at. El orden exacto es el deGET /events. - Si una dirección está pausada, lo que pasa mientras tanto no se le manda, ni al reactivarla: recupéralo con
GET /events. - Solo direcciones
httpsen el puerto 443, sin usuario ni contraseña en la dirección. Docus no sigue redirecciones ni manda avisos a direcciones de redes internas. - «Mandar una prueba» envía un evento
pingfirmado igual que los reales, para probar tu verificación. Su cuerpo es{ "id", "type": "ping", "created_at", "livemode", "data": { "endpoint_id": "…" } }: no traedata.contracty llega sin la cabeceraDocus-Delivery-Id(no es una entrega: no se reintenta ni se guarda). - Si los avisos a una dirección fallan durante un día entero, Docus le escribe a quien administra las integraciones de tu empresa.
Preguntar por los eventos (polling)
Si prefieres preguntar en vez de recibir avisos, o para recuperar lo que se perdió, GET /events devuelve los mismos eventos, del más viejo al más nuevo, de los últimos 30 días. Guarda el next_cursor de cada respuesta y mándalo como after la vez siguiente: recibes solo lo nuevo. Si has_more es true, pregunta de nuevo enseguida. Un evento aparece unos segundos después de ocurrir (5 s), para que ninguno se cuele detrás de tu cursor. Hay eventos desde que la API está activa en tu cuenta.
