Copiar página

Formularios PDF: del widget al documento firmado

Llenar es parte de firmar. Con la API v3 subes un PDF que ya trae su formulario (AcroForm), sus widgets se vuelven campos con nombre, tipo y posición, le pones a cada campo el rol que lo llena, y al emitir el documento decides qué va prellenado y bloqueado. El firmante llena lo suyo sobre el mismo PDF y firma; tú lees después campo por campo quién lo llenó, cuándo y con qué valor. Seis pasos, todos por API, ninguno en el panel.

Prerrequisitos: una API key con scopes documents:read y documents:write (Autenticación). En sandbox (allsign_test_sk_…) nada tiene validez legal y no se cobran créditos (Entornos). Referencia completa de cada llamada en Templates y Documents.

Los ejemplos usan $ALLSIGN_API_KEY. Defínela una vez con tu key de sandbox: export ALLSIGN_API_KEY="allsign_test_sk_…" (en PowerShell: $env:ALLSIGN_API_KEY="allsign_test_sk_…").

1 · Subir el PDF y leer los campos detectados

Crea la plantilla con el PDF en base64. Si trae formulario, cada widget entra como campo con source: "acroform"; la respuesta ya dice cuántos campos hay y cuántos siguen sin rol (readiness: "pending" hasta que todos tengan uno). Una caja de firma cuyo nombre sugiere el rol (firma_cliente) nace con el rol Cliente.

curl -X POST "https://api.allsign.io/v3/templates" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Content-Type: application/json" \
  -d '{"name":"Contrato de compraventa","file":{"content":"<base64 del PDF>","fileType":"pdf","name":"contrato-compraventa.pdf"}}'

201 — la plantilla:

{
  "object": "template",
  "id": "tmpl_3f9c1a2b4d5e6f70",
  "livemode": true,
  "name": "Contrato de compraventa",
  "description": null,
  "fileType": "pdf",
  "originalFilename": "contrato-compraventa.pdf",
  "aiEditable": true,
  "variableCount": 0,
  "fieldCount": 5,
  "unassignedFieldCount": 4,
  "roleCount": 1,
  "pendingCandidates": 0,
  "readiness": "pending",
  "tags": [],
  "category": null,
  "usageCount": 0,
  "lastUsedAt": null,
  "currentVersion": 1,
  "previewUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/preview",
  "downloadUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/download",
  "createdAt": "2026-09-20T17:58:03Z",
  "updatedAt": "2026-09-20T17:58:03Z"
}

Lee los campos: name es la clave estable (la del widget), type es text, date, checkbox, radio, select, signature, initials o stamp, y areas trae página y rectángulo en % del papel (varias áreas = el mismo dato repetido en el PDF).

curl "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70/fields" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"

200:

{
  "object": "list",
  "templateId": "tmpl_3f9c1a2b4d5e6f70",
  "pageCount": 1,
  "data": [
    {
      "object": "template_field",
      "name": "nombre",
      "type": "text",
      "role": null,
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "areas": [
        {
          "page": 1,
          "rect": {
            "x": 12.5,
            "y": 18.2,
            "width": 40,
            "height": 3.1
          }
        }
      ]
    },
    {
      "object": "template_field",
      "name": "rfc",
      "type": "text",
      "role": null,
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "areas": [
        {
          "page": 1,
          "rect": {
            "x": 12.5,
            "y": 24.6,
            "width": 40,
            "height": 3.1
          }
        }
      ]
    },
    {
      "object": "template_field",
      "name": "fecha",
      "type": "date",
      "role": null,
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "areas": [
        {
          "page": 1,
          "rect": {
            "x": 12.5,
            "y": 31,
            "width": 20,
            "height": 3.1
          }
        }
      ]
    },
    {
      "object": "template_field",
      "name": "acepta",
      "type": "checkbox",
      "role": null,
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "areas": [
        {
          "page": 1,
          "rect": {
            "x": 12.5,
            "y": 70.4,
            "width": 2.4,
            "height": 1.6
          }
        }
      ]
    },
    {
      "object": "template_field",
      "name": "firma_cliente",
      "type": "signature",
      "role": "Cliente",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "areas": [
        {
          "page": 1,
          "rect": {
            "x": 12.5,
            "y": 80,
            "width": 30,
            "height": 8
          }
        }
      ]
    }
  ],
  "unassignedCount": 4,
  "hasMore": false,
  "livemode": true
}

Un PDF XFA (LiveCycle) responde 422 UNSUPPORTED_FORM_XFA; un PDF que ya trae una firma digital, 422 con PDF_ALREADY_SIGNED — editarlo la invalidaría. Sube la versión sin firmar o un PDF plano.

2 · Asignar roles y comprobar que la plantilla está lista

El rol es el dueño del campo; el firmante se deriva del rol al emitir. Declara los roles en orden y asigna cada campo con PATCH …/fields/{name} (también puedes fijar required y una label para el firmante). Cuando unassignedFieldCount llega a 0, la plantilla queda readiness: "ready".

curl -X PUT "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70/roles" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Content-Type: application/json" \
  -d '{"roles":["Cliente"]}'

for name in nombre rfc fecha acepta; do
  curl -X PATCH "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70/fields/$name" \
    -H "Authorization: Bearer $ALLSIGN_API_KEY" \
    -H "AllSign-Version: 2026-07-11" \
    -H "Content-Type: application/json" \
    -d '{"role":"Cliente"}'
done

curl "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"

200 — lista para emitir:

{
  "object": "template",
  "id": "tmpl_3f9c1a2b4d5e6f70",
  "livemode": true,
  "name": "Contrato de compraventa",
  "description": null,
  "fileType": "pdf",
  "originalFilename": "contrato-compraventa.pdf",
  "aiEditable": true,
  "variableCount": 0,
  "fieldCount": 5,
  "unassignedFieldCount": 0,
  "roleCount": 1,
  "pendingCandidates": 0,
  "readiness": "ready",
  "tags": [],
  "category": null,
  "usageCount": 0,
  "lastUsedAt": null,
  "currentVersion": 1,
  "previewUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/preview",
  "downloadUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/download",
  "createdAt": "2026-09-20T17:58:03Z",
  "updatedAt": "2026-09-20T18:01:40Z"
}

Si prefieres versionar el layout completo en tu repositorio, PUT …/fields lo reemplaza de forma declarativa e idempotente; y POST …/fields:from-document copia los campos y roles de un documento ya armado en el panel ("guardar como plantilla", por API). Copia la estructura, no los datos: los valores que ese documento tenía no pasan a la plantilla.

3 · Revisar el PDF con los widgets vivos, o aplanado

Antes de emitir, baja el PDF tal como lo verá el firmante. Con format=form conserva los widgets del formulario para revisarlos en Acrobat o en tu propio visor; con format=flat los hornea (sin campos editables).

curl "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70/file?format=form" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -o contrato-form.pdf

curl "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70/file?format=flat" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -o contrato-flat.pdf

200 — application/pdf con Content-Disposition: attachment.

4 · Crear el documento con valores prellenados y bloqueados por firmante

Cada firmante toma un rol por roleName y hereda sus campos. values prellena campos por nombre (texto, fecha YYYY-MM-DD o true/false en casillas) y readOnly lista los que el firmante puede ver pero no cambiar: el caso del asesor que manda el contrato con el RFC ya puesto. Manda Idempotency-Key (Idempotencia).

curl -X POST "https://api.allsign.io/v3/documents" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @crear.json

Cuerpo (crear.json):

{
  "name": "Contrato de compraventa — Ana López",
  "templateId": "tmpl_3f9c1a2b4d5e6f70",
  "signers": [
    {
      "roleName": "Cliente",
      "email": "ana@cliente.mx",
      "name": "Ana López",
      "values": {
        "rfc": "XAXX010101000",
        "fecha": "2026-09-20"
      },
      "readOnly": [
        "rfc"
      ]
    }
  ]
}

Los campos del documento se materializan en segundos. Los prellenados traen su valor y filledBy: "owner"; el resto espera al firmante (filledBy: null).

curl "https://api.allsign.io/v3/documents/doc_8f2c1e0a9b7d4c6e5f3a2b1c0d9e8f7a/fields" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"

200:

{
  "object": "list",
  "documentId": "doc_8f2c1e0a9b7d4c6e5f3a2b1c0d9e8f7a",
  "data": [
    {
      "id": "fie_0f1e2d3c4b5a69788796a5b4c3d2e1f0",
      "object": "document_field",
      "name": "nombre",
      "type": "text",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 18.2,
        "width": 40,
        "height": 3.1
      },
      "position": 1,
      "status": "pending",
      "value": {
        "text": null,
        "checked": null
      },
      "filledBy": null,
      "filledAt": null
    },
    {
      "id": "fie_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d",
      "object": "document_field",
      "name": "rfc",
      "type": "text",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": true,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 24.6,
        "width": 40,
        "height": 3.1
      },
      "position": 1,
      "status": "pending",
      "value": {
        "text": "XAXX010101000",
        "checked": null
      },
      "filledBy": "owner",
      "filledAt": "2026-09-20T18:02:11Z"
    },
    {
      "id": "fie_1a2b3c4d5e6f708192a3b4c5d6e7f809",
      "object": "document_field",
      "name": "fecha",
      "type": "date",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 31,
        "width": 20,
        "height": 3.1
      },
      "position": 1,
      "status": "pending",
      "value": {
        "text": "2026-09-20",
        "checked": null
      },
      "filledBy": "owner",
      "filledAt": "2026-09-20T18:02:11Z"
    },
    {
      "id": "fie_2b3c4d5e6f708192a3b4c5d6e7f8091a",
      "object": "document_field",
      "name": "acepta",
      "type": "checkbox",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 70.4,
        "width": 2.4,
        "height": 1.6
      },
      "position": 1,
      "status": "pending",
      "value": {
        "text": null,
        "checked": null
      },
      "filledBy": null,
      "filledAt": null
    },
    {
      "id": "fie_3c4d5e6f708192a3b4c5d6e7f8091a2b",
      "object": "document_field",
      "name": "firma_cliente",
      "type": "signature",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 80,
        "width": 30,
        "height": 8
      },
      "position": 1,
      "status": "pending",
      "value": {
        "text": null,
        "checked": null
      },
      "filledBy": null,
      "filledAt": null
    }
  ],
  "unassignedCount": 0,
  "hasMore": false,
  "livemode": true
}

También puedes prellenar o bloquear después de crear, campo por campo, con PATCH /v3/documents/{id}/fields/{fieldId} {"value": …, "readOnly": true}; y dibujar campos nuevos con POST …/fields. Luego envía con POST /v3/documents/{id}/send.

5 · Leer el estado: quién lo llenó, cuándo y con qué valor

El firmante llena sus campos sobre el PDF y firma. Cuando el documento llega a completed (o en cualquier momento antes), vuelve a leer los campos: value trae lo capturado, status pasa a signed, y filledBy / filledAt dicen quién puso cada valor y cuándo — "owner" lo que prellenaste tú, "signer" lo que escribió el firmante dueño (signerId).

curl "https://api.allsign.io/v3/documents/doc_8f2c1e0a9b7d4c6e5f3a2b1c0d9e8f7a/fields" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"

200:

{
  "object": "list",
  "documentId": "doc_8f2c1e0a9b7d4c6e5f3a2b1c0d9e8f7a",
  "data": [
    {
      "id": "fie_0f1e2d3c4b5a69788796a5b4c3d2e1f0",
      "object": "document_field",
      "name": "nombre",
      "type": "text",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 18.2,
        "width": 40,
        "height": 3.1
      },
      "position": 1,
      "status": "signed",
      "value": {
        "text": "Ana López",
        "checked": null
      },
      "filledBy": "signer",
      "filledAt": "2026-09-21T10:15:42Z"
    },
    {
      "id": "fie_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d",
      "object": "document_field",
      "name": "rfc",
      "type": "text",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": true,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 24.6,
        "width": 40,
        "height": 3.1
      },
      "position": 1,
      "status": "signed",
      "value": {
        "text": "XAXX010101000",
        "checked": null
      },
      "filledBy": "owner",
      "filledAt": "2026-09-20T18:02:11Z"
    },
    {
      "id": "fie_1a2b3c4d5e6f708192a3b4c5d6e7f809",
      "object": "document_field",
      "name": "fecha",
      "type": "date",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 31,
        "width": 20,
        "height": 3.1
      },
      "position": 1,
      "status": "signed",
      "value": {
        "text": "2026-09-20",
        "checked": null
      },
      "filledBy": "owner",
      "filledAt": "2026-09-20T18:02:11Z"
    },
    {
      "id": "fie_2b3c4d5e6f708192a3b4c5d6e7f8091a",
      "object": "document_field",
      "name": "acepta",
      "type": "checkbox",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 70.4,
        "width": 2.4,
        "height": 1.6
      },
      "position": 1,
      "status": "signed",
      "value": {
        "text": null,
        "checked": true
      },
      "filledBy": "signer",
      "filledAt": "2026-09-21T10:16:03Z"
    },
    {
      "id": "fie_3c4d5e6f708192a3b4c5d6e7f8091a2b",
      "object": "document_field",
      "name": "firma_cliente",
      "type": "signature",
      "role": "Cliente",
      "signerId": "sgr_1b2c3d4e5f60718293a4b5c6d7e8f901",
      "required": true,
      "label": null,
      "options": null,
      "group": null,
      "source": "acroform",
      "readOnly": false,
      "page": 1,
      "rect": {
        "x": 12.5,
        "y": 80,
        "width": 30,
        "height": 8
      },
      "position": 1,
      "status": "signed",
      "value": {
        "text": null,
        "checked": null
      },
      "filledBy": null,
      "filledAt": null
    }
  ],
  "unassignedCount": 0,
  "hasMore": false,
  "livemode": true
}

Un campo firmado es inmutable: PATCH, DELETE y fields:assign-role responden 409 FIELD_CONFLICT. Para enterarte sin consultar, suscríbete al webhook document.completed (Webhooks).

6 · Descargar el documento

Antes de enviar, GET /v3/documents/{id}/file?format=form devuelve el PDF con los widgets vivos y lo prellenado (para que el emisor lo revise) y format=flat lo hornea con los valores actuales. Después de la firma, el PDF con validez legal es la evidencia (GET /v3/documents/{id}/evidence): todos los widgets van horneados con lo capturado, más la constancia NOM-151.

curl "https://api.allsign.io/v3/documents/doc_8f2c1e0a9b7d4c6e5f3a2b1c0d9e8f7a/file?format=flat" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -o contrato-flat.pdf

200 — application/pdf con Content-Disposition: attachment.

Cuando el PDF cambia: reemplazarlo sin perder los campos

Meses después el contrato se actualiza —una cláusula nueva, otro logo— y la plantilla ya tiene sus campos colocados y sus roles asignados. POST /v3/templates/{id}/file cambia el archivo y los conserva: las áreas se guardan en porcentaje de página, así que un PDF con otro tamaño de hoja no las mueve. El archivo viaja en base64, igual que al crear la plantilla, y solo se acepta PDF sobre PDF (este endpoint no convierte DOCX↔PDF).

curl -X POST "https://api.allsign.io/v3/templates/tmpl_3f9c1a2b4d5e6f70/file" \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Content-Type: application/json" \
  -d '{"file":{"content":"<base64 del PDF nuevo>","fileType":"pdf","name":"contrato-compraventa-2026.pdf"}}'

200 — el archivo nuevo tiene al menos las mismas páginas, así que nada se podó:

{
  "object": "template_file_replace",
  "template": {
    "object": "template",
    "id": "tmpl_3f9c1a2b4d5e6f70",
    "livemode": true,
    "name": "Contrato de compraventa",
    "description": null,
    "fileType": "pdf",
    "originalFilename": "contrato-compraventa-2026.pdf",
    "aiEditable": true,
    "variableCount": 0,
    "fieldCount": 5,
    "unassignedFieldCount": 0,
    "roleCount": 1,
    "pendingCandidates": 0,
    "readiness": "ready",
    "tags": [],
    "category": null,
    "usageCount": 0,
    "lastUsedAt": null,
    "currentVersion": 2,
    "previewUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/preview",
    "downloadUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/download",
    "createdAt": "2026-09-20T17:58:03Z",
    "updatedAt": "2026-10-02T09:12:55Z"
  },
  "pageCount": 2,
  "prunedFields": [],
  "livemode": true
}

Lo único que sí puede romperse es el número de páginas: un campo que vivía en una página que el archivo nuevo ya no tiene se poda —se borra esa área, nunca la plantilla ni el resto de las áreas de ese campo— y la respuesta lo dice en prunedFields. Revísalo siempre: removed: true significa que el campo se quedó sin ninguna área y hay que volver a colocarlo.

200 — el PDF nuevo trae una página menos y la casilla que vivía en la segunda se cayó:

{
  "object": "template_file_replace",
  "template": {
    "object": "template",
    "id": "tmpl_3f9c1a2b4d5e6f70",
    "livemode": true,
    "name": "Contrato de compraventa",
    "description": null,
    "fileType": "pdf",
    "originalFilename": "contrato-compraventa-corto.pdf",
    "aiEditable": true,
    "variableCount": 0,
    "fieldCount": 4,
    "unassignedFieldCount": 0,
    "roleCount": 1,
    "pendingCandidates": 0,
    "readiness": "ready",
    "tags": [],
    "category": null,
    "usageCount": 0,
    "lastUsedAt": null,
    "currentVersion": 3,
    "previewUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/preview",
    "downloadUrl": "/templates/tmpl_3f9c1a2b4d5e6f70/download",
    "createdAt": "2026-09-20T17:58:03Z",
    "updatedAt": "2026-10-09T11:40:18Z"
  },
  "pageCount": 1,
  "prunedFields": [
    {
      "name": "acepta",
      "pages": [
        2
      ],
      "removed": true
    }
  ],
  "livemode": true
}

Cada reemplazo avanza currentVersion y guarda una versión: GET /v3/templates/{id}/versions las lista de la más reciente a la más antigua, con el layout y un summary para comparar dos de un vistazo. Un PDF XFA responde 422 UNSUPPORTED_FORM_XFA y una plantilla DOCX, 422.

Qué sigue