Firmas
El dominio de Firmas (Firmas / signatures) es el motor de firma electrónica propio de Cannahub. La webapp posee todo el flujo: Strapi almacena los registros (plantillas y solicitudes), Medusa/Strapi almacenan los archivos PDF/PNG, y el BFF de la webapp renderiza, captura y sella los documentos. En esta página veremos cómo consultar plantillas de documentos, crear y firmar solicitudes, y gestionar la firma reutilizable de cada usuario. Todos los endpoints son rutas BFF propias de la webapp y requieren autenticación Bearer JWT junto con el header Tenant-id.
Estas rutas viven bajo /api/signatures. La mayoría de las operaciones de escritura exigen el header Tenant-id: {tenantId} para aislar los datos por club (tenant).
El modelo SignatureRequest
Una solicitud de firma (SignatureRequest) representa un documento enviado a uno o más firmantes. Toma una foto (snapshot) de los campos de la plantilla al momento de crearse, de modo que editar la plantilla nunca rompe solicitudes en curso. Cuando todos los firmantes requeridos completan su firma, el BFF sella el PDF y publica signedFileUrl.
Propiedades
- Name
id- Type
- string
- Description
Identificador único de la solicitud de firma.
- Name
templateId- Type
- string
- Description
ID de la plantilla de la que deriva la solicitud.
- Name
templateName- Type
- string
- Description
Nombre de la plantilla al momento de crear la solicitud.
- Name
documentType- Type
- enum
- Description
Tipo de documento. Puede ser uno de:
- CONSENT
- CONTRACT
- MEMBERSHIP
- PRESCRIPTION
- OTHER
- Name
title- Type
- string
- Description
Título del documento mostrado a los firmantes.
- Name
status- Type
- enum
- Description
Estado de la solicitud. Puede ser uno de:
- PENDING: Creada, esperando la primera firma
- IN_PROGRESS: Firmada parcialmente
- COMPLETED: Todos firmaron, PDF sellado
- DECLINED: Un firmante rechazó
- EXPIRED: Venció el plazo de firma
- CANCELLED: Cancelada por el emisor
- Name
signingOrder- Type
- enum
- Description
Orden de firma. Puede ser uno de:
- PARALLEL: Todos pueden firmar en cualquier orden
- SEQUENTIAL: Se firma por turnos, según
order
- Name
signers- Type
- Signer[]
- Description
Lista de firmantes de la solicitud.
- Name
id- Type
- string
- Description
Identificador único del firmante.
- Name
roleKey- Type
- string
- Description
Clave del rol del firmante (ej.
member,patient,doctor).
- Name
roleLabel- Type
- string
- Description
Etiqueta legible del rol.
- Name
name- Type
- string
- Description
Nombre del firmante.
- Name
email- Type
- string
- Description
Email del firmante.
- Name
userId- Type
- string
- Description
ID de usuario de la plataforma, cuando se conoce.
- Name
order- Type
- number
- Description
Orden de firma (base 1), usado con
SEQUENTIAL.
- Name
status- Type
- enum
- Description
Estado del firmante. Puede ser uno de:
- PENDING
- VIEWED
- SIGNED
- DECLINED
- Name
viewedAt- Type
- string
- Description
Timestamp de cuando el firmante vio el documento.
- Name
signedAt- Type
- string
- Description
Timestamp de cuando el firmante firmó.
- Name
consentedAt- Type
- string
- Description
Timestamp de aceptación del consentimiento de firma electrónica (evidencia).
- Name
declinedReason- Type
- string
- Description
Motivo del rechazo, si corresponde.
- Name
fieldValues- Type
- object
- Description
Valores capturados, indexados por ID de campo.
- Name
signatureImageUrl- Type
- string
- Description
URL del PNG de la firma capturada.
- Name
ip- Type
- string
- Description
Dirección IP del firmante al firmar.
- Name
userAgent- Type
- string
- Description
User agent del firmante al firmar.
- Name
fieldsSnapshot- Type
- SignatureField[]
- Description
Snapshot de los campos de la plantilla al crear la solicitud.
- Name
baseFileUrl- Type
- string
- Description
URL del PDF base (sin firmar).
- Name
pageCount- Type
- number
- Description
Cantidad de páginas del PDF.
- Name
signedFileUrl- Type
- string
- Description
URL del PDF sellado, disponible una vez completada la firma.
- Name
signedFileHash- Type
- string
- Description
Hash del PDF sellado (integridad).
- Name
auditTrail- Type
- AuditEntry[]
- Description
Registro de auditoría de eventos (creación, firma, rechazo, sellado).
- Name
creatorId- Type
- string
- Description
ID del usuario que creó la solicitud.
- Name
resourceType- Type
- string
- Description
Tipo de recurso vinculado (ej.
member,patient,order).
- Name
resourceId- Type
- string
- Description
ID del recurso vinculado (usado para surfacear el documento en el perfil del miembro).
- Name
completedAt- Type
- string
- Description
Timestamp de finalización de la solicitud.
- Name
expiresAt- Type
- string
- Description
Timestamp de vencimiento de la solicitud.
- Name
createdAt- Type
- string
- Description
Timestamp de creación.
- Name
updatedAt- Type
- string
- Description
Timestamp de última actualización.
Ejemplo de SignatureRequest
{
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"templateName": "Consentimiento REPROCANN",
"documentType": "CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "IN_PROGRESS",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"id": "9f1c2a3b-0001-4c2d-8e11-abc123456789",
"roleKey": "patient",
"roleLabel": "Paciente",
"name": "María González",
"email": "maria.gonzalez@email.com",
"userId": "usr_01HQ8XYZABC123",
"order": 1,
"status": "SIGNED",
"signedAt": "2024-03-20T14:22:00Z",
"consentedAt": "2024-03-20T14:22:00Z",
"fieldValues": {
"field_name": "María González",
"field_dni": "35123456"
},
"signatureImageUrl": "https://storage.cannahub.tech/signatures/maria-g.png",
"ip": "190.210.10.5",
"userAgent": "Mozilla/5.0"
},
{
"id": "9f1c2a3b-0002-4c2d-8e11-def987654321",
"roleKey": "doctor",
"roleLabel": "Médico",
"name": "Dr. Roberto Fernández",
"email": "r.fernandez@clinica.com",
"userId": "usr_01HQ8DOCTOR99",
"order": 2,
"status": "PENDING",
"fieldValues": {}
}
],
"fieldsSnapshot": [
{
"id": "field_name",
"type": "fullName",
"roleKey": "patient",
"page": 1,
"x": 0.2,
"y": 0.6,
"w": 0.3,
"h": 0.04,
"required": true,
"prefillKey": "patientFullName"
}
],
"baseFileUrl": "https://media.cannahub.tech/uploads/reprocann-consent.pdf",
"pageCount": 2,
"auditTrail": [
{ "event": "request.created", "actorId": "usr_01HQ8DOCTOR99", "at": "2024-03-19T10:00:00Z" },
{ "event": "signer.signed", "signerId": "9f1c2a3b-0001-4c2d-8e11-abc123456789", "at": "2024-03-20T14:22:00Z" }
],
"creatorId": "usr_01HQ8DOCTOR99",
"resourceType": "member",
"resourceId": "usr_01HQ8XYZABC123",
"createdAt": "2024-03-19T10:00:00Z",
"updatedAt": "2024-03-20T14:22:00Z"
}
El modelo SignatureTemplate
Una plantilla de documento (SignatureTemplate) es la entrada de catálogo desde la que se generan las solicitudes. Define el PDF base, los roles de firmante y los campos posicionados. Las plantillas GLOBAL (documentos oficiales de REPROCANN) son compartidas por todos los tenants y están bloqueadas; las plantillas TENANT son creadas por cada club.
Propiedades
- Name
id- Type
- string
- Description
Identificador único de la plantilla.
- Name
name- Type
- string
- Description
Nombre de la plantilla.
- Name
documentType- Type
- enum
- Description
Tipo de documento. Puede ser uno de:
- CONSENT
- CONTRACT
- MEMBERSHIP
- PRESCRIPTION
- OTHER
- Name
active- Type
- boolean
- Description
Si la plantilla está activa y disponible para generar solicitudes.
- Name
description- Type
- string
- Description
Descripción de la plantilla.
- Name
fileUrl- Type
- string
- Description
URL del PDF base de la plantilla.
- Name
fileName- Type
- string
- Description
Nombre del archivo PDF base.
- Name
pageCount- Type
- number
- Description
Cantidad de páginas del PDF.
- Name
roles- Type
- SignerRole[]
- Description
Roles de firmante definidos en la plantilla.
- Name
key- Type
- string
- Description
Clave del rol (ej.
member,doctor).
- Name
label- Type
- string
- Description
Etiqueta legible del rol.
- Name
color- Type
- string
- Description
Color de acento (hex) para tintar los campos del rol.
- Name
fields- Type
- SignatureField[]
- Description
Campos posicionados sobre el documento.
- Name
id- Type
- string
- Description
Identificador del campo.
- Name
type- Type
- enum
- Description
Tipo de campo. Puede ser uno de:
- signature
- initials
- text
- date
- checkbox
- fullName
- Name
roleKey- Type
- string
- Description
Rol que debe completar el campo (coincide con
SignerRole.key).
- Name
page- Type
- number
- Description
Índice de página (base 1).
- Name
x- Type
- number
- Description
Coordenada X como fracción del ancho de página (0..1).
- Name
y- Type
- number
- Description
Coordenada Y como fracción del alto de página (0..1, origen arriba).
- Name
w- Type
- number
- Description
Ancho del campo como fracción del ancho de página.
- Name
h- Type
- number
- Description
Alto del campo como fracción del alto de página.
- Name
required- Type
- boolean
- Description
Si el campo es obligatorio.
- Name
label- Type
- string
- Description
Etiqueta del campo.
- Name
fontSize- Type
- number
- Description
Tamaño de fuente como fracción del alto de página (solo campos de texto).
- Name
prefillKey- Type
- enum
- Description
Origen de autocompletado del campo. Puede ser uno de:
- patientFullName
- patientFirstName
- patientLastName
- patientDni
- patientRecordNumber
- clubAddress
- clubCity
- clubProvince
- doctorName
- doctorLicense
- doctorDni
- doctorAddress
- signPlace
- signDay
- signMonth
- signYear
- Name
readOnly- Type
- boolean
- Description
Bloquea el campo ante ediciones del firmante (usado con
prefillKey).
- Name
scope- Type
- enum
- Description
Alcance de la plantilla. Puede ser uno de:
- GLOBAL: Oficial, compartida entre tenants, bloqueada
- TENANT: Creada por el club
- Name
locked- Type
- boolean
- Description
Si está bloqueada (no editable/eliminable por administradores del tenant).
- Name
key- Type
- string
- Description
Slug estable usado por flujos para resolver la plantilla (ej.
reprocann_consent).
- Name
createdAt- Type
- string
- Description
Timestamp de creación.
- Name
updatedAt- Type
- string
- Description
Timestamp de última actualización.
Ejemplo de SignatureTemplate
{
"id": "tpl_01HQ9CONTRACT",
"name": "Contrato de asociación",
"documentType": "CONTRACT",
"active": true,
"description": "Contrato de adhesión al club de cannabis medicinal.",
"fileUrl": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf",
"fileName": "contrato-asociacion.pdf",
"pageCount": 3,
"roles": [
{ "key": "member", "label": "Socio", "color": "#7C9A92" }
],
"fields": [
{
"id": "field_fullname",
"type": "fullName",
"roleKey": "member",
"page": 1,
"x": 0.15,
"y": 0.72,
"w": 0.35,
"h": 0.04,
"required": true,
"prefillKey": "patientFullName",
"readOnly": true
},
{
"id": "field_signature",
"type": "signature",
"roleKey": "member",
"page": 3,
"x": 0.55,
"y": 0.85,
"w": 0.3,
"h": 0.08,
"required": true
}
],
"scope": "TENANT",
"locked": false,
"key": "contrato_asociacion",
"createdAt": "2024-02-01T09:00:00Z",
"updatedAt": "2024-02-10T12:00:00Z"
}
El modelo "Mi firma"
La firma guardada (mi firma) es un PNG reutilizable que cada usuario puede almacenar una sola vez y reutilizar en todos sus documentos. Para miembros se guarda en la metadata del customer de Medusa (savedSignatureUrl); para usuarios administrativos (doctor/staff/admin) se guarda en la metadata del usuario de Medusa. El endpoint devuelve tanto la URL como el PNG embebido en base64 (data URL), evitando bloqueos por CORS al reutilizarlo desde el cliente.
Propiedades
- Name
signatureUrl- Type
- string
- Description
URL del PNG de la firma guardada. Es
nullsi el usuario aún no guardó ninguna.
- Name
signatureDataUrl- Type
- string
- Description
PNG de la firma inlineado como data URL base64, listo para reutilizar sin fetch cross-origin. Es
nullsi no hay firma o si no pudo cargarse.
Ejemplo de mi firma
{
"signatureUrl": "https://storage.cannahub.tech/signatures/usr_01HQ8XYZABC123.png",
"signatureDataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}
Listar solicitudes de firma
Retorna las solicitudes de firma del tenant. Los administradores y staff ven todas las solicitudes del club; los médicos (rol DOCTOR) quedan acotados a las solicitudes que crearon o a las de sus propios pacientes.
Atributos opcionales (query params)
- Name
status- Type
- enum
- Description
Filtra por estado de la solicitud (
PENDING,IN_PROGRESS,COMPLETED,DECLINED,EXPIRED,CANCELLED).
- Name
templateId- Type
- string
- Description
Filtra por ID de plantilla.
- Name
resourceId- Type
- string
- Description
Filtra por ID de recurso vinculado (ej. un miembro).
- Name
q- Type
- string
- Description
Búsqueda de texto libre.
Request
curl -G https://api.cannahub.tech/api/signatures/requests \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-d status=PENDING
Response
{
"requests": [
{
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"templateName": "Consentimiento REPROCANN",
"documentType": "CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "PENDING",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"id": "9f1c2a3b-0001-4c2d-8e11-abc123456789",
"roleKey": "patient",
"roleLabel": "Paciente",
"name": "María González",
"email": "maria.gonzalez@email.com",
"order": 1,
"status": "PENDING",
"fieldValues": {}
}
],
"fieldsSnapshot": [],
"resourceType": "member",
"resourceId": "usr_01HQ8XYZABC123",
"createdAt": "2024-03-19T10:00:00Z"
}
],
"count": 1
}
Crear una solicitud de firma
Crea una nueva solicitud de firma a partir de una plantilla activa. Solo pueden crear solicitudes usuarios con rol de administrador, staff o médico. El servidor expande el body: toma un snapshot de los campos de la plantilla, precompleta (prefill) los datos conocidos de cada firmante (nombre y DNI del paciente, dirección del club, datos del médico emisor), vincula la solicitud al miembro/paciente firmante y notifica por email a quienes pueden firmar ya (todos en PARALLEL, el primero en SEQUENTIAL).
Atributos requeridos
- Name
templateId- Type
- string
- Description
ID de la plantilla activa a usar.
- Name
signers- Type
- array
- Description
Lista no vacía de firmantes. Cada firmante requiere
emailyroleKey.- Name
roleKey- Type
- string
- Description
Clave del rol del firmante.
- Name
email- Type
- string
- Description
Email del firmante.
- Name
name- Type
- string
- Description
Nombre del firmante.
- Name
roleLabel- Type
- string
- Description
Etiqueta del rol (si se omite se resuelve desde la plantilla).
- Name
userId- Type
- string
- Description
ID de usuario de la plataforma, para prefill y vinculación.
- Name
order- Type
- number
- Description
Orden de firma.
- Name
fieldValues- Type
- object
- Description
Valores iniciales para los campos propios del rol, fusionados sobre el prefill del servidor.
Atributos opcionales
- Name
title- Type
- string
- Description
Título del documento (por defecto usa el nombre de la plantilla).
- Name
signingOrder- Type
- enum
- Description
Orden de firma:
PARALLEL(por defecto) oSEQUENTIAL.
- Name
sendEmail- Type
- boolean
- Description
Si es
false, no se envían emails de invitación. Por defecto se envían.
- Name
resourceType- Type
- string
- Description
Tipo de recurso a vincular (por defecto
membersi hay firmante miembro/paciente).
- Name
resourceId- Type
- string
- Description
ID del recurso a vincular (por defecto el
userIddel firmante miembro/paciente).
Request
curl -X POST https://api.cannahub.tech/api/signatures/requests \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"templateId": "tpl_01HQ9CONSENT",
"title": "Consentimiento REPROCANN - María González",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"roleKey": "patient",
"name": "María González",
"email": "maria.gonzalez@email.com",
"userId": "usr_01HQ8XYZABC123"
},
{
"roleKey": "doctor",
"name": "Dr. Roberto Fernández",
"email": "r.fernandez@clinica.com"
}
]
}'
Response (201)
{
"request": {
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"templateName": "Consentimiento REPROCANN",
"documentType": "CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "PENDING",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"id": "9f1c2a3b-0001-4c2d-8e11-abc123456789",
"roleKey": "patient",
"roleLabel": "Paciente",
"name": "María González",
"email": "maria.gonzalez@email.com",
"userId": "usr_01HQ8XYZABC123",
"order": 1,
"status": "PENDING",
"fieldValues": { "field_name": "María González", "field_dni": "35123456" }
},
{
"id": "9f1c2a3b-0002-4c2d-8e11-def987654321",
"roleKey": "doctor",
"roleLabel": "Médico",
"name": "Dr. Roberto Fernández",
"email": "r.fernandez@clinica.com",
"order": 2,
"status": "PENDING",
"fieldValues": {}
}
],
"creatorId": "usr_01HQ8DOCTOR99",
"resourceType": "member",
"resourceId": "usr_01HQ8XYZABC123",
"createdAt": "2024-03-19T10:00:00Z"
}
}
Mis solicitudes de firma
Retorna únicamente las solicitudes donde el usuario autenticado es firmante o creador. Es privacy-safe: la identidad se resuelve del lado del servidor (getProfileSSR) y el filtrado ocurre en el BFF, de modo que un miembro nunca ve documentos de otro. No recibe parámetros.
Request
curl "https://api.cannahub.tech/api/signatures/requests/mine" \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"requests": [
{
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "PENDING",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"id": "9f1c2a3b-0001-4c2d-8e11-abc123456789",
"roleKey": "patient",
"roleLabel": "Paciente",
"name": "María González",
"email": "maria.gonzalez@email.com",
"userId": "usr_01HQ8XYZABC123",
"order": 1,
"status": "PENDING",
"fieldValues": {}
}
],
"fieldsSnapshot": []
}
],
"count": 1
}
Obtener una solicitud de firma
Retorna una solicitud individual. Solo es legible por un participante (creador o firmante) o por un usuario admin/staff/médico. Para solicitudes no completadas cuyo baseFileUrl apunte a un origen Medusa efímero ya migrado, el BFF sana la URL usando el fileUrl actual de la plantilla, para que el PDF cargue.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la solicitud de firma.
Request
curl "https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001" \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"request": {
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"templateName": "Consentimiento REPROCANN",
"documentType": "CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "IN_PROGRESS",
"signingOrder": "SEQUENTIAL",
"signers": [],
"fieldsSnapshot": [],
"baseFileUrl": "https://media.cannahub.tech/uploads/reprocann-consent.pdf",
"pageCount": 2,
"resourceType": "member",
"resourceId": "usr_01HQ8XYZABC123"
}
}
Actualizar una solicitud de firma
Actualiza metadata o estado de una solicitud. Restringido a usuarios admin/staff/médico. El body es un SignatureRequestUpdate (parcial de la solicitud, sin id), por lo que se pueden enviar campos como status, title, expiresAt, etc.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la solicitud de firma.
Body
- Name
status- Type
- enum
- Description
Nuevo estado de la solicitud.
- Name
title- Type
- string
- Description
Nuevo título del documento.
- Name
expiresAt- Type
- string
- Description
Nuevo timestamp de vencimiento.
Request
curl -X PATCH https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001 \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{ "status": "CANCELLED" }'
Response
{
"request": {
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "CANCELLED",
"signingOrder": "SEQUENTIAL",
"signers": [],
"fieldsSnapshot": []
}
}
Eliminar una solicitud de firma
Cancela / elimina una solicitud de firma. Restringido a usuarios admin/staff/médico.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la solicitud de firma.
Request
curl -X DELETE https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001 \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"success": true
}
Firmar un documento
Envía la firma del firmante autenticado. La firma es identity-critical: el firmante DEBE ser el usuario autenticado, nunca se confía en un signerId provisto por el cliente. Requiere consentimiento explícito. El servidor valida los campos requeridos del rol, sube el PNG de la firma capturada, y cuando todos los firmantes requeridos completaron, sella el PDF y marca la solicitud como COMPLETED.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la solicitud de firma.
Body
- Name
consent- Type
- boolean
- Description
Consentimiento explícito de firma electrónica. Debe ser
true.
- Name
fieldValues- Type
- object
- Description
Valores de los campos completados, indexados por ID de campo.
- Name
signatureImage- Type
- string
- Description
PNG de la firma como data URL; el servidor lo sube al almacenamiento.
- Name
signerId- Type
- string
- Description
Opcional; el servidor matchea al usuario autenticado por email cuando se omite.
Request
curl -X POST https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001/sign \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"consent": true,
"fieldValues": { "field_name": "María González", "field_dni": "35123456" },
"signatureImage": "data:image/png;base64,iVBORw0KGgo..."
}'
Response
{
"request": {
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "COMPLETED",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"id": "9f1c2a3b-0001-4c2d-8e11-abc123456789",
"roleKey": "patient",
"roleLabel": "Paciente",
"name": "María González",
"email": "maria.gonzalez@email.com",
"order": 1,
"status": "SIGNED",
"signedAt": "2024-03-20T14:22:00Z",
"consentedAt": "2024-03-20T14:22:00Z",
"signatureImageUrl": "https://storage.cannahub.tech/signatures/maria-g.png",
"fieldValues": { "field_name": "María González", "field_dni": "35123456" }
}
],
"fieldsSnapshot": [],
"signedFileUrl": "https://storage.cannahub.tech/sealed/req_01HQ9SIGN001.pdf",
"signedFileHash": "sha256:9a1b2c3d...",
"completedAt": "2024-03-20T14:22:00Z"
}
}
Rechazar un documento
Permite al firmante autenticado rechazar la firma de un documento. El firmante debe estar en la solicitud y no haber firmado aún. Se registra el motivo en el audit trail y se notifica al creador.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la solicitud de firma.
Body
- Name
reason- Type
- string
- Description
Motivo del rechazo.
- Name
signerId- Type
- string
- Description
Opcional; el servidor matchea al usuario autenticado cuando se omite.
Request
curl -X POST https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001/decline \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{ "reason": "Los datos del contrato son incorrectos" }'
Response
{
"request": {
"id": "req_01HQ9SIGN001",
"templateId": "tpl_01HQ9CONSENT",
"title": "Consentimiento REPROCANN - María González",
"status": "DECLINED",
"signingOrder": "SEQUENTIAL",
"signers": [
{
"id": "9f1c2a3b-0001-4c2d-8e11-abc123456789",
"roleKey": "patient",
"roleLabel": "Paciente",
"name": "María González",
"email": "maria.gonzalez@email.com",
"order": 1,
"status": "DECLINED",
"declinedReason": "Los datos del contrato son incorrectos",
"fieldValues": {}
}
],
"fieldsSnapshot": []
}
}
Reenviar la invitación de firma
Reenvía la invitación de firma a los firmantes pendientes. Restringido a usuarios admin/staff/médico. En SEQUENTIAL solo se notifica al firmante de turno; en PARALLEL se notifica a todo firmante que aún no firmó ni rechazó. No recibe body.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la solicitud de firma.
Request
curl -X POST https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001/resend \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"success": true
}
Listar plantillas de documento
Retorna las plantillas de documento del tenant.
Atributos opcionales (query params)
- Name
documentType- Type
- enum
- Description
Filtra por tipo de documento (
CONSENT,CONTRACT,MEMBERSHIP,PRESCRIPTION,OTHER).
- Name
active- Type
- boolean
- Description
Filtra por estado activo (
true/false).
- Name
key- Type
- string
- Description
Resuelve una plantilla por su slug estable (ej.
reprocann_consent).
- Name
q- Type
- string
- Description
Búsqueda de texto libre.
Request
curl -G https://api.cannahub.tech/api/signatures/templates \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-d documentType=CONTRACT \
-d active=true
Response
{
"templates": [
{
"id": "tpl_01HQ9CONTRACT",
"name": "Contrato de asociación",
"documentType": "CONTRACT",
"active": true,
"fileUrl": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf",
"pageCount": 3,
"scope": "TENANT",
"key": "contrato_asociacion"
}
],
"count": 1
}
Crear una plantilla
Crea una plantilla de documento. Restringido a usuarios admin/staff. El name y el documentType son requeridos; fields, si se envía, debe ser un array; una key que coincida con una clave global reservada es rechazada (un tenant no puede shadowear un documento oficial).
Atributos requeridos
- Name
name- Type
- string
- Description
Nombre de la plantilla.
- Name
documentType- Type
- enum
- Description
Tipo de documento.
Atributos opcionales
- Name
active- Type
- boolean
- Description
Si la plantilla está activa (por defecto
true).
- Name
description- Type
- string
- Description
Descripción de la plantilla.
- Name
fileUrl- Type
- string
- Description
URL del PDF base (típicamente el retornado por
/templates/upload).
- Name
fileName- Type
- string
- Description
Nombre del archivo PDF.
- Name
pageCount- Type
- number
- Description
Cantidad de páginas.
- Name
roles- Type
- SignerRole[]
- Description
Roles de firmante de la plantilla.
- Name
fields- Type
- SignatureField[]
- Description
Campos posicionados del documento.
- Name
key- Type
- string
- Description
Slug de flujo para resolver la plantilla desde onboarding/flujos médicos.
Request
curl -X POST https://api.cannahub.tech/api/signatures/templates \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"name": "Contrato de asociación",
"documentType": "CONTRACT",
"fileUrl": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf",
"pageCount": 3,
"roles": [{ "key": "member", "label": "Socio" }],
"key": "contrato_asociacion"
}'
Response (201)
{
"template": {
"id": "tpl_01HQ9CONTRACT",
"name": "Contrato de asociación",
"documentType": "CONTRACT",
"active": true,
"fileUrl": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf",
"pageCount": 3,
"roles": [{ "key": "member", "label": "Socio" }],
"scope": "TENANT",
"key": "contrato_asociacion"
}
}
Subir el PDF base de una plantilla
Sube el PDF base de una plantilla al almacenamiento de media de Strapi (durable). Restringido a usuarios admin/staff. Recibe multipart/form-data: el archivo debe enviarse en el campo files. Retorna { files: [{ url }] } — la URL resultante se guarda luego como fileUrl de la plantilla.
Body (multipart/form-data)
- Name
files- Type
- file
- Description
El archivo PDF a subir. El servidor toma el primer archivo del campo
files.
Request
curl -X POST https://api.cannahub.tech/api/signatures/templates/upload \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-F "files=@contrato-asociacion.pdf"
Response
{
"files": [
{ "url": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf" }
]
}
Obtener una plantilla
Retorna una plantilla de documento individual.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la plantilla.
Request
curl "https://api.cannahub.tech/api/signatures/templates/tpl_01HQ9CONTRACT" \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"template": {
"id": "tpl_01HQ9CONTRACT",
"name": "Contrato de asociación",
"documentType": "CONTRACT",
"active": true,
"fileUrl": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf",
"pageCount": 3,
"roles": [{ "key": "member", "label": "Socio" }],
"scope": "TENANT",
"key": "contrato_asociacion"
}
}
Actualizar una plantilla
Actualiza una plantilla de documento. Restringido a usuarios admin/staff. El body es un SignatureTemplateUpdate (parcial de los campos de creación).
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la plantilla.
Body
- Name
name- Type
- string
- Description
Nuevo nombre de la plantilla.
- Name
active- Type
- boolean
- Description
Nuevo estado activo.
- Name
fields- Type
- SignatureField[]
- Description
Nuevos campos posicionados.
- Name
roles- Type
- SignerRole[]
- Description
Nuevos roles de firmante.
Request
curl -X PUT https://api.cannahub.tech/api/signatures/templates/tpl_01HQ9CONTRACT \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{ "name": "Contrato de asociación 2024", "active": true }'
Response
{
"template": {
"id": "tpl_01HQ9CONTRACT",
"name": "Contrato de asociación 2024",
"documentType": "CONTRACT",
"active": true,
"fileUrl": "https://media.cannahub.tech/uploads/contrato-asociacion.pdf",
"pageCount": 3,
"scope": "TENANT",
"key": "contrato_asociacion"
}
}
Eliminar una plantilla
Elimina una plantilla de documento. Restringido a usuarios admin/staff.
Atributos de ruta
- Name
id- Type
- string
- Description
ID de la plantilla.
Request
curl -X DELETE https://api.cannahub.tech/api/signatures/templates/tpl_01HQ9CONTRACT \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"success": true
}
Quién soy (para firmas)
Endpoint "who am I" agnóstico del rol para la UI de firmas. A diferencia de /api/profile (solo customer), resuelve vía getProfileSSR, por lo que funciona para member, doctor, staff y admin por igual. Lo usa el modal de creación de solicitudes para prefillear al co-firmante profesional y habilitar firmar-al-enviar. No recibe parámetros.
Response
- Name
user- Type
- object
- Description
Datos del usuario autenticado.
- Name
id- Type
- string
- Description
ID del usuario.
- Name
email- Type
- string
- Description
Email del usuario.
- Name
firstName- Type
- string
- Description
Nombre.
- Name
lastName- Type
- string
- Description
Apellido.
- Name
role- Type
- string
- Description
Rol del usuario (member, doctor, staff, admin).
Request
curl "https://api.cannahub.tech/api/signatures/me" \
-H "Authorization: Bearer {token}"
Response
{
"user": {
"id": "usr_01HQ8DOCTOR99",
"email": "r.fernandez@clinica.com",
"firstName": "Roberto",
"lastName": "Fernández",
"role": "doctor"
}
}
Obtener mi firma guardada
Retorna la firma reutilizable del usuario autenticado: la URL del PNG y el mismo PNG inlineado como data URL base64 (para reusarlo sin fetch cross-origin). Funciona tanto para miembros como para usuarios administrativos, ramificando internamente según sea admin. No recibe parámetros.
Response
- Name
signatureUrl- Type
- string
- Description
URL del PNG de la firma guardada, o
null.
- Name
signatureDataUrl- Type
- string
- Description
PNG inlineado como data URL base64, o
null.
Request
curl "https://api.cannahub.tech/api/signatures/my-signature" \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"signatureUrl": "https://storage.cannahub.tech/signatures/usr_01HQ8XYZABC123.png",
"signatureDataUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
}
Guardar mi firma
Sube y persiste una nueva firma reutilizable para el usuario autenticado. El signatureImage debe ser un data URL PNG. El servidor sube el PNG y guarda la URL en la metadata del customer (miembros) o del usuario (admin/doctor/staff).
Body
- Name
signatureImage- Type
- string
- Description
PNG de la firma como data URL (debe comenzar con
data:).
Request
curl -X PUT https://api.cannahub.tech/api/signatures/my-signature \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{ "signatureImage": "data:image/png;base64,iVBORw0KGgo..." }'
Response
{
"signatureUrl": "https://storage.cannahub.tech/signatures/usr_01HQ8XYZABC123.png"
}
Asegurar documentos de onboarding
Endpoint self-service de onboarding: asegura que existan los documentos estándar para el miembro actual, para que pueda firmarlos inline en el wizard. A diferencia de la ruta de creación normal (solo admin/staff/médico), aquí el propio miembro crea sus documentos de un solo firmante (solo para plantillas activas resueltas por una key conocida).
La respuesta es determinística: exactamente una entrada por key solicitada, en el orden de entrada. request es null cuando ninguna plantilla activa resuelve para esa key, o cuando la plantilla es multi-firmante (los documentos REPROCANN de consentimiento y DDJJ son enviados por el médico, no autocreados). Máximo 10 keys.
Body
- Name
keys- Type
- string[]
- Description
Lista de slugs de plantilla a asegurar (ej.
contrato_asociacion,reprocann_consent). Requerido, máximo 10.
Response
- Name
docs- Type
- OnboardingDocSlot[]
- Description
Un slot por key solicitada, en orden.
- Name
key- Type
- string
- Description
La key solicitada.
- Name
request- Type
- SignatureRequest
- Description
La solicitud creada o reutilizada, o
nullsi no aplica.
Request
curl -X POST https://api.cannahub.tech/api/signatures/onboarding/ensure \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{ "keys": ["contrato_asociacion", "reprocann_consent"] }'
Response
{
"docs": [
{
"key": "contrato_asociacion",
"request": {
"id": "req_01HQ9ONBOARD01",
"templateId": "tpl_01HQ9CONTRACT",
"templateName": "Contrato de asociación",
"documentType": "CONTRACT",
"title": "Contrato de asociación",
"status": "PENDING",
"signingOrder": "PARALLEL",
"signers": [
{
"id": "9f1c2a3b-0003-4c2d-8e11-onboard0001",
"roleKey": "member",
"roleLabel": "Socio",
"name": "María González",
"email": "maria.gonzalez@email.com",
"userId": "usr_01HQ8XYZABC123",
"order": 1,
"status": "PENDING",
"fieldValues": { "field_fullname": "María González" }
}
],
"fieldsSnapshot": [],
"resourceType": "member",
"resourceId": "usr_01HQ8XYZABC123"
}
},
{
"key": "reprocann_consent",
"request": null
}
]
}
Proxy de archivo de firma
Proxy same-origin para PDFs de firma. Los servidores estáticos de Medusa y Strapi no envían headers CORS, por lo que el navegador bloquea a pdf.js al intentar traer el documento cross-origin. Esta ruta trae el PDF desde el mismo origen. Está acotada a los dos orígenes de backend configurados (Medusa + Strapi) y solo acepta https; si el upstream oficial de REPROCANN no está disponible, sirve un PDF de consentimiento/DDJJ embebido como fallback. Responde con Content-Type: application/pdf.
Atributos requeridos (query params)
- Name
url- Type
- string
- Description
URL absoluta del PDF a proxear. Debe ser
httpsy pertenecer a un origen permitido (Medusa o Strapi).
Request
curl -G "https://api.cannahub.tech/api/signatures/file" \
--data-urlencode "url=https://media.cannahub.tech/uploads/reprocann-consent.pdf" \
-H "Authorization: Bearer {token}" \
--output documento.pdf
Response
Content-Type: application/pdf
Cache-Control: private, max-age=300
%PDF-1.7
...(bytes binarios del PDF)...
React Query Hooks
Cannahub provee hooks de React Query para gestionar firmas. Estos hooks interactúan con las rutas BFF de /api/signatures y manejan caché, invalidación y errores automáticamente.
Query Keys
// /features/Club/Documents/hooks/keys.ts
export const signatureRequestKeys = {
all: ['signature-requests'] as const,
lists: () => [...signatureRequestKeys.all, 'list'] as const,
list: (filters?: SignatureRequestFilters) => [...signatureRequestKeys.lists(), filters] as const,
mine: () => [...signatureRequestKeys.all, 'mine'] as const,
details: () => [...signatureRequestKeys.all, 'detail'] as const,
detail: (id: string) => [...signatureRequestKeys.details(), id] as const,
}
export const signatureTemplateKeys = {
all: ['signature-templates'] as const,
lists: () => [...signatureTemplateKeys.all, 'list'] as const,
list: (filters?: SignatureTemplateFilters) => [...signatureTemplateKeys.lists(), filters] as const,
details: () => [...signatureTemplateKeys.all, 'detail'] as const,
detail: (id: string) => [...signatureTemplateKeys.details(), id] as const,
}
Queries
import {
useSignatureRequestsQuery,
useMySignatureRequestsQuery,
useSignatureRequestQuery,
useSignatureTemplatesQuery,
} from '@/features/Club/Documents/hooks'
// Listar solicitudes del tenant con filtros opcionales
const { data } = useSignatureRequestsQuery({ status: 'PENDING' })
// Solicitudes del usuario autenticado (firmante o creador)
const { data: mine } = useMySignatureRequestsQuery()
// Una solicitud individual
const { data: request } = useSignatureRequestQuery('req_01HQ9SIGN001')
// Plantillas de documento
const { data: templates } = useSignatureTemplatesQuery({ documentType: 'CONTRACT', active: true })
Mutations
import {
useCreateSignatureRequestMutation,
useSignDocumentMutation,
useDeclineDocumentMutation,
useResendSignatureMutation,
useSaveMySignatureMutation,
} from '@/features/Club/Documents/hooks'
// Crear una solicitud (admin/staff/doctor)
const { mutate: createRequest } = useCreateSignatureRequestMutation()
createRequest({
templateId: 'tpl_01HQ9CONSENT',
signingOrder: 'SEQUENTIAL',
signers: [
{ roleKey: 'patient', name: 'María González', email: 'maria.gonzalez@email.com', userId: 'usr_01HQ8XYZABC123' },
{ roleKey: 'doctor', name: 'Dr. Roberto Fernández', email: 'r.fernandez@clinica.com' }
]
})
// Firmar un documento
const { mutate: sign } = useSignDocumentMutation()
sign({ id: 'req_01HQ9SIGN001', consent: true, fieldValues: {}, signatureImage: pngDataUrl })
// Rechazar un documento
const { mutate: decline } = useDeclineDocumentMutation()
decline({ id: 'req_01HQ9SIGN001', reason: 'Datos incorrectos' })
// Guardar mi firma reutilizable
const { mutate: saveSignature } = useSaveMySignatureMutation()
saveSignature({ signatureImage: pngDataUrl })
Cache Invalidation
Todas las mutations invalidan automáticamente las query keys relevantes al finalizar con éxito:
// Tras una firma o actualización:
queryClient.invalidateQueries({ queryKey: signatureRequestKeys.detail(id) })
queryClient.invalidateQueries({ queryKey: signatureRequestKeys.all })
Ciclo de vida de una solicitud
El estado de una solicitud de firma se recalcula a partir del estado de sus firmantes. Cuando todos los firmantes requeridos completan, el BFF sella el PDF y publica signedFileUrl + signedFileHash.
Notas de seguridad
- Firma identity-critical: el firmante debe ser el usuario autenticado; nunca se confía en un
signerIdprovisto por el cliente. - Scope por tenant: casi todas las escrituras requieren
Tenant-id, aislando los datos por club. - Scope de médicos: los médicos solo ven solicitudes que crearon o de sus propios pacientes.
- Privacidad de miembros:
/requests/minefiltra en el servidor para que un miembro nunca vea documentos de otro. - Proxy acotado:
/filesolo proxeahttpsdesde los orígenes Medusa/Strapi configurados, evitando un proxy abierto / SSRF.