Inicio · API
Una llave, una lectura y un aviso. Nada más, a propósito.
La API pública de SektorSign sirve para que otro sistema —un CRM, un ERP, un panel interno— sepa por dónde va un contrato y pueda arrancar uno nuevo sin que nadie copie datos a mano. Es pequeña porque lo importante es que no cambie.
Dirección base y versión
Todo cuelga de una dirección con la versión dentro. Cuelga aparte de la API de la aplicación a propósito: lo que la aplicación usa por dentro puede cambiar cuando el producto lo necesite; lo que hay aquí es un compromiso con sistemas de terceros y no se rompe sin avisar.
https://sektorsign.com/api/public/v1Cuando haya una v2, la v1 seguirá contestando. Un campo nuevo puede aparecer en cualquier momento —tu integración debe ignorar los que no conozca—, pero ninguno de los que ya están se quita ni cambia de significado dentro de la misma versión.
Autenticación
Cada petición lleva una llave del espacio de trabajo en la cabecera:
Authorization: Bearer ss_live_…Cómo se consigue una llave
- Entra en la aplicación y abre Ajustes → Integraciones.
- Crea una llave y ponle un nombre que diga para qué es («CRM», «facturación»): así sabrás cuál revocar el día que haga falta.
- Cópiala en ese momento. Solo se enseña una vez: en la base queda únicamente su SHA-256, así que ni el soporte puede recuperarla. Si se pierde, se revoca y se crea otra.
Lo que la llave puede y no puede
Una llave da acceso a un espacio de trabajo entero, sin distinguir persona. No es una sesión: no lleva cookies, no pasa por la comprobación anti-CSRF y no caduca sola. Y no esquiva al plano de control: si el espacio está suspendido o congelado, la llave deja de valer en el acto.
Trátala como una contraseña: en una variable de entorno del servidor, nunca en el código de un navegador ni en un repositorio. El prefijo existe para que la reconozcas de un vistazo en un registro y para que un buscador de secretos la encuentre antes que otro.
Comprobar que la llave vive
GET/ping
Lo primero que conviene probar. Devuelve el espacio al que da acceso.
curl https://sektorsign.com/api/public/v1/ping \
-H "Authorization: Bearer $SEKTORSIGN_API_KEY"
{ "ok": true, "workspaceId": "7c1f…" }Listar contratos
GET/contracts
Los contratos del espacio, del más recientemente movido al más antiguo.
Parámetros
| limit | Cuántos devolver. Entre 1 y 100; por defecto, 25. |
|---|---|
| status | Filtra por estado. Uno de los de abajo. |
curl "https://sektorsign.com/api/public/v1/contracts?status=sent&limit=50" \
-H "Authorization: Bearer $SEKTORSIGN_API_KEY"Todavía no hay paginación por cursor: hay página con tope y nada más. Cuando alguien tenga bastantes contratos como para que importe, se añadirá sin romper esto.
Leer un contrato
GET/contracts/:id
curl https://sektorsign.com/api/public/v1/contracts/7c1f… \
-H "Authorization: Bearer $SEKTORSIGN_API_KEY"Lo que devuelve un contrato
Es deliberadamente más pobre que lo que ve la aplicación: sin claves de almacenamiento, sin hashes de token, sin IP y sin el historial completo. Lo que un sistema de fuera necesita para reaccionar, y nada más. Cada campo que se añadiera aquí sería un compromiso que ya no se podría quitar.
{
"contract": {
"id": "7c1f…",
"titulo": "Propuesta de servicios",
"estado": "sent",
"creadoEl": "2026-08-19T09:12:44.106Z",
"enviadoEl": "2026-08-19T09:20:01.550Z",
"firmadoEl": null,
"venceEl": "2026-09-19",
"firmantes": [
{
"nombre": "Marta Ruiz",
"email": "marta@ejemplo.com",
"papel": "signer",
"turno": 1,
"haFirmado": false
}
]
}
}Los estados
| draft | Existe, con su PDF, pero no está preparado para enviarse. |
|---|---|
| prepared | Tiene firmantes y campos colocados; falta enviarlo. |
| sent | Se han enviado las invitaciones. Nadie lo ha abierto todavía. |
| viewed | Alguien ha abierto su enlace. |
| signed | No falta ninguna firma. |
| completed | Además existen el PDF firmado y el informe de evidencia. |
| expired | Pasó su fecha de vencimiento sin completarse. |
Listar plantillas
GET/templates
Para saber con cuál crear. Devuelve su patrón de título y su versión.
{
"templates": [
{
"id": "31ab…",
"name": "Propuesta estándar",
"titlePattern": "Propuesta · {{cliente}}",
"version": 3
}
]
}Crear un borrador desde una plantilla
POST/templates/:id/contracts
Lo único que la API escribe. Las variables que no envíes se quedan con sus llaves puestas —`{{cliente}}`— para que se vea qué falta en lugar de generar un hueco silencioso. Si prefieres escribir el título entero en vez de usar el patrón de la plantilla, manda `titulo`.
curl -X POST https://sektorsign.com/api/public/v1/templates/31ab…/contracts \
-H "Authorization: Bearer $SEKTORSIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "variables": { "cliente": "Marta Ruiz" } }'
201 Created
{ "contractId": "9d0e…" }Crea un borrador, y ahí se para
Sigue haciendo falta que una persona entre, revise, ponga los firmantes y lo envíe. Un programa puede preparar el trabajo; mandarle un contrato a alguien sigue siendo una decisión con nombre y apellidos detrás.
El contrato queda atribuido a quien creó la plantilla. Una llave no es una persona, y la autoría tiene que caer en alguien de verdad: quien montó la plantilla es la elección honesta, porque es su trabajo el que se está reutilizando.
Lo que la API no hace, y por qué
- No firma. Firmar exige que una persona vea el documento, acepte un texto de consentimiento y deje su IP y la hora del servidor en la evidencia. Una llave de API no es una persona, y un contrato firmado por un programa sería exactamente lo que este producto sostiene que no puede pasar.
- No borra nada. Todo lo irreversible se queda en la aplicación, con sesión y con una confirmación delante.
- No descarga documentos. El PDF firmado y el informe de evidencia se descargan desde la aplicación, donde consta quién los descargó.
Errores
Siempre el mismo cuerpo, con el código HTTP que le corresponde. El texto de `message` está pensado para una persona y puede cambiar; lo que no cambia dentro de una versión es `code`.
{ "error": { "code": "unauthorized", "message": "Esa llave de API no vale." } }| Código | Cuándo |
|---|---|
| 400 validation_error | El cuerpo no cumple. `fields` dice qué campo y por qué. |
| 401 unauthorized | Falta la llave, no vale, está revocada o el espacio no está activo. |
| 404 not_found | Ese contrato o esa plantilla no existen en este espacio. |
| 500 internal_error | Fallo nuestro. Reintenta con espera creciente. |
Webhooks
Preguntar cada minuto por si un contrato cambió es caro y llega tarde. Un webhook le da la vuelta: SektorSign llama a tu dirección cuando pasa algo. Se dan de alta en Ajustes → Integraciones, y solo por https —un aviso por HTTP viaja en claro por internet con el título de un contrato y el nombre de quien firma dentro.
Eventos
| contract.sent | Se han enviado las invitaciones. |
|---|---|
| contract.viewed | Alguien ha abierto el enlace de firma por primera vez. |
| contract.signed | Ya no falta ninguna firma. |
| contract.completed | Están listos el PDF firmado y el informe de evidencia. |
| contract.expired | Venció sin completarse. |
Es un subconjunto pequeño y estable del historial del contrato: los momentos en los que un sistema de fuera tiene algo que hacer. El historial entero no se expone porque cada evento nuevo del producto se convertiría en un compromiso público que ya no se podría cambiar.
El aviso se encola dentro de la misma transacción que provoca el hecho. Si el contrato no llega a enviarse, tampoco sale el aviso de que se envió.
El cuerpo de la entrega
{
"evento": "contract.signed",
"datos": { "contractId": "7c1f…", "firmantes": 2 }
}Las cabeceras
| x-sektorsign-signature | La firma, como `sha256=<hex>`. |
|---|---|
| x-sektorsign-timestamp | Segundos desde el epoch, en el momento del envío. |
| x-sektorsign-event | El nombre del evento, para enrutar sin abrir el cuerpo. |
| x-sektorsign-delivery | Identificador único de esta entrega. Úsalo para no procesar dos veces. |
Verificar la firma
Se firma `<momento>.<cuerpo>` con HMAC-SHA256 y el secreto del webhook. El momento entra dentro de la firma para que una entrega interceptada no pueda reenviarse mañana: compáralo con tu reloj y descarta lo viejo.
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCIA = 300 // segundos
export function valida(cuerpoCrudo, cabeceras, secreto) {
const momento = cabeceras['x-sektorsign-timestamp']
const recibida = cabeceras['x-sektorsign-signature']
if (!momento || !recibida) return false
// Fuera lo viejo: una entrega interceptada no se reenvía mañana.
const edad = Math.abs(Math.floor(Date.now() / 1000) - Number(momento))
if (!Number.isFinite(edad) || edad > TOLERANCIA) return false
const esperada =
'sha256=' +
createHmac('sha256', secreto)
.update(`${momento}.${cuerpoCrudo}`)
.digest('hex')
const a = Buffer.from(esperada)
const b = Buffer.from(recibida)
return a.length === b.length && timingSafeEqual(a, b)
}Compara con el cuerpo **crudo**, tal y como llegó. Si lo pasas por `JSON.parse` y lo vuelves a serializar, un espacio de diferencia tira la firma abajo.
Y compara en tiempo constante. Un `===` sobre una firma filtra, byte a byte, cuánto has acertado.
Reintentos y apagado automático
Tu extremo tiene diez segundos para contestar 2xx. Si no contesta o contesta otra cosa, se reintenta con espera creciente: un minuto, cinco, veinticinco. (4)
Un webhook que acumula fallos seguidos se desactiva solo, y la aplicación lo enseña apagado con su último código de respuesta. Reactivarlo a mano perdona los fallos acumulados, para que no se apague otra vez al primer tropiezo por la cuenta de la vez anterior. (10)
Diseña tu extremo para recibir el mismo evento dos veces. Un reintento después de que tu servidor procesara pero no llegara a contestar es exactamente eso.
¿Falta algo?
Esta API es pequeña por decisión, no por descuido: crece cuando alguien la necesita de verdad, no por catálogo. Si te falta un endpoint para integrar SektorSign con tu sistema, escríbenos y lo hablamos. info@sektorsign.com