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.


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
      • email
    • 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 null si 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 null si 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..."
}

GET/api/signatures/requests

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

GET
/api/signatures/requests
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
}

POST/api/signatures/requests

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 email y roleKey.

    • 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) o SEQUENTIAL.

  • 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 member si hay firmante miembro/paciente).

  • Name
    resourceId
    Type
    string
    Description

    ID del recurso a vincular (por defecto el userId del firmante miembro/paciente).

Request

POST
/api/signatures/requests
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"
  }
}

GET/api/signatures/requests/mine

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

GET
/api/signatures/requests/mine
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
}

GET/api/signatures/requests/:id

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

GET
/api/signatures/requests/:id
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"
  }
}

PATCH/api/signatures/requests/:id

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

PATCH
/api/signatures/requests/:id
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": []
  }
}

DELETE/api/signatures/requests/:id

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

DELETE
/api/signatures/requests/:id
curl -X DELETE https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001 \
  -H "Authorization: Bearer {token}" \
  -H "Tenant-id: {tenantId}"

Response

{
  "success": true
}

POST/api/signatures/requests/:id/sign

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

POST
/api/signatures/requests/:id/sign
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"
  }
}

POST/api/signatures/requests/:id/decline

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

POST
/api/signatures/requests/:id/decline
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": []
  }
}

POST/api/signatures/requests/:id/resend

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

POST
/api/signatures/requests/:id/resend
curl -X POST https://api.cannahub.tech/api/signatures/requests/req_01HQ9SIGN001/resend \
  -H "Authorization: Bearer {token}" \
  -H "Tenant-id: {tenantId}"

Response

{
  "success": true
}

GET/api/signatures/templates

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

GET
/api/signatures/templates
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
}

POST/api/signatures/templates

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

POST
/api/signatures/templates
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"
  }
}

POST/api/signatures/templates/upload

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

POST
/api/signatures/templates/upload
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" }
  ]
}

GET/api/signatures/templates/:id

Obtener una plantilla

Retorna una plantilla de documento individual.

Atributos de ruta

  • Name
    id
    Type
    string
    Description

    ID de la plantilla.

Request

GET
/api/signatures/templates/:id
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"
  }
}

PUT/api/signatures/templates/:id

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

PUT
/api/signatures/templates/:id
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"
  }
}

DELETE/api/signatures/templates/:id

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

DELETE
/api/signatures/templates/:id
curl -X DELETE https://api.cannahub.tech/api/signatures/templates/tpl_01HQ9CONTRACT \
  -H "Authorization: Bearer {token}" \
  -H "Tenant-id: {tenantId}"

Response

{
  "success": true
}

GET/api/signatures/me

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

GET
/api/signatures/me
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"
  }
}

GET/api/signatures/my-signature

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

GET
/api/signatures/my-signature
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..."
}

PUT/api/signatures/my-signature

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

PUT
/api/signatures/my-signature
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"
}

POST/api/signatures/onboarding/ensure

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 null si no aplica.

Request

POST
/api/signatures/onboarding/ensure
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
    }
  ]
}

GET/api/signatures/file

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 https y pertenecer a un origen permitido (Medusa o Strapi).

Request

GET
/api/signatures/file
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.

Loading diagram...

Notas de seguridad

  • Firma identity-critical: el firmante debe ser el usuario autenticado; nunca se confía en un signerId provisto 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/mine filtra en el servidor para que un miembro nunca vea documentos de otro.
  • Proxy acotado: /file solo proxea https desde los orígenes Medusa/Strapi configurados, evitando un proxy abierto / SSRF.

Was this page helpful?