---
title: "Pon la firma dentro de tu app — Receta de la API v3 de AllSign"
type: how-to
canonical_url: "https://allsign.io/developers/docs/recetas/firma-embebida-en-tu-app"
api_version: "2026-07-11"
agent_rules: [webhook-no-polling, idempotency-key, una-base-url, error-por-code, reintento-429]
last_verified: "2026-10-08T12:17:58.225Z"
---

Receta

# Pon la firma dentro de tu app

**Objetivo:** Tu usuario firma sin salir de tu página, en una sesión de firma que solo se abre desde tu dominio.

Para: frontend · nivel intermedio · 15 min · Necesitas: API key de sandbox (solo en tu backend) · Tu dominio, aquí https://app.ejemplo.com · El SDK @allsign/embedded en tu frontend

## Para agentes y asistentes de IA

-   **Para saber que un documento quedó firmado, espera el webhook `document.completed`; no consultes `GET /v3/documents/{id}` en un bucle.** → [Webhooks](https://allsign.io/developers/docs/webhooks)
-   **Todo `POST` que crea, cobra o envía algo lleva `Idempotency-Key` con un UUID v4 nuevo por operación.** → [Idempotencia](https://allsign.io/developers/docs/idempotency)
-   **La base URL es siempre `https://api.allsign.io/v3`; no existe una URL de sandbox aparte.** → [Entornos](https://allsign.io/developers/docs/environments)
-   **Los errores son `application/problem+json` (RFC 9457) con un `code` estable y append-only; `detail` es texto técnico en inglés que puede cambiar.** → [Errores](https://allsign.io/developers/docs/errors)
-   **Ante un `429`, espera los segundos de `Retry-After` antes de reintentar; si el `429` no trae `Retry-After`, no lo reintentes.** → [Rate limits](https://allsign.io/developers/docs/rate-limits)

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:17 (hora CDMX) · test `docs-checks-v3/tests/recetas/firma-embebida-en-tu-app.receta.spec.ts`

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 Crea el documento con su firmante y su caja de firma

POST `/documents` [ver en la referencia](https://allsign.io/developers/docs/endpoints/documents#create-document)

### Qué haces

Todo lo que lleva tu API key pasa en tu backend: crear, enviar y abrir la sesión. El navegador nunca ve la key.

### Qué mirar

-   `id` = `"doc_e6d57f83692e4ff5ad07f57f6bd6a117"`
-   `status` = `"draft"`

**cURL**

```
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 '{
  "name": "Contrato de servicios — Ana Torres",
  "file": {
    "content": "'"$(base64 < contrato.pdf | tr -d '\n')"'",
    "fileType": "pdf",
    "name": "contrato.pdf"
  },
  "signers": [
    {
      "email": "ana@ejemplo.com",
      "name": "Ana Torres"
    }
  ],
  "fields": [
    {
      "email": "ana@ejemplo.com",
      "pageNumber": 1,
      "position": {
        "x": 80,
        "y": 620
      }
    }
  ]
}'
```

**Node**

```
import { randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';

const respuesta = await fetch('https://api.allsign.io/v3/documents', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': randomUUID(),
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        name: 'Contrato de servicios — Ana Torres',
        file: {
            content: readFileSync('contrato.pdf').toString('base64'),
            fileType: 'pdf',
            name: 'contrato.pdf',
        },
        signers: [
            {
                email: 'ana@ejemplo.com',
                name: 'Ana Torres',
            },
        ],
        fields: [
            {
                email: 'ana@ejemplo.com',
                pageNumber: 1,
                position: {
                    x: 80,
                    y: 620,
                },
            },
        ],
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import base64
import os
import uuid
from pathlib import Path

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/documents",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "name": "Contrato de servicios — Ana Torres",
        "file": {
            "content": base64.b64encode(Path("contrato.pdf").read_bytes()).decode(),
            "fileType": "pdf",
            "name": "contrato.pdf",
        },
        "signers": [
            {
                "email": "ana@ejemplo.com",
                "name": "Ana Torres",
            },
        ],
        "fields": [
            {
                "email": "ana@ejemplo.com",
                "pageNumber": 1,
                "position": {
                    "x": 80,
                    "y": 620,
                },
            },
        ],
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 201**

```
{
  "livemode": false,
  "id": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
  "object": "document",
  "name": "Contrato de servicios — Ana Torres",
  "status": "draft",
  "documentType": "EDITABLE",
  "signerCount": 1,
  "signedCount": 0,
  "ownerId": "usr_3ec54866cfb843df8a85d593a4f4e474",
  "orgId": "949b6951-4058-4a55-8931-6ba302d6824f",
  "folderId": null,
  "expiresAt": null,
  "signingOrder": "parallel",
  "currentStage": null,
  "expirationReminders": null,
  "templateId": null,
  "templateVersionId": null,
  "parentDocumentId": null,
  "createdAt": "2026-07-11T18:00:00.000000Z",
  "updatedAt": "2026-07-11T18:00:00.000000Z"
}
```

## 2 Envíalo sin mandar invitaciones

POST `/documents/{document_id}/send` [ver en la referencia](https://allsign.io/developers/docs/endpoints/documents#send-document)

### Qué haces

recipients vacío pasa el documento a awaiting\_signatures sin mandarle correo a nadie: la firma va a pasar dentro de tu página. Si lo envías sin recipients, AllSign invita por correo a cada firmante.

### Qué mirar

-   `status` = `"awaiting_signatures"`

### En producción

Con una key live este envío consume créditos aunque no invite a nadie; en sandbox no se cobra nada.

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/documents/doc_e6d57f83692e4ff5ad07f57f6bd6a117/send' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "recipients": []
}'
```

**Node**

```
import { randomUUID } from 'node:crypto';

const respuesta = await fetch('https://api.allsign.io/v3/documents/doc_e6d57f83692e4ff5ad07f57f6bd6a117/send', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': randomUUID(),
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        recipients: [],
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os
import uuid

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/documents/doc_e6d57f83692e4ff5ad07f57f6bd6a117/send",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "recipients": [],
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "livemode": false,
  "id": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
  "object": "document",
  "name": "Contrato de servicios — Ana Torres",
  "status": "awaiting_signatures",
  "documentType": "EDITABLE",
  "signerCount": 1,
  "signedCount": 0,
  "ownerId": "usr_3ec54866cfb843df8a85d593a4f4e474",
  "orgId": "949b6951-4058-4a55-8931-6ba302d6824f",
  "folderId": null,
  "expiresAt": null,
  "signingOrder": "parallel",
  "currentStage": null,
  "expirationReminders": null,
  "templateId": null,
  "templateVersionId": null,
  "parentDocumentId": null,
  "createdAt": "2026-07-11T18:00:00.000000Z",
  "updatedAt": "2026-07-11T18:00:01.126000Z"
}
```

## 3 Confirma que nadie recibió invitación

GET `/documents/{document_id}/signers` [ver en la referencia](https://allsign.io/developers/docs/endpoints/documents#list-signers)

### Qué haces

pending: el firmante está en espera y no se le mandó nada. Con un envío normal aquí verías sent.

### Qué mirar

-   `data.0.status` = `"pending"`

**cURL**

```
curl 'https://api.allsign.io/v3/documents/doc_e6d57f83692e4ff5ad07f57f6bd6a117/signers' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11"
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/documents/doc_e6d57f83692e4ff5ad07f57f6bd6a117/signers', {
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
    },
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os

import requests

respuesta = requests.get(
    "https://api.allsign.io/v3/documents/doc_e6d57f83692e4ff5ad07f57f6bd6a117/signers",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "object": "list",
  "data": [
    {
      "livemode": false,
      "id": "sgr_fdcf573b43b24b2484b1a795ffff848f",
      "object": "signer",
      "documentId": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
      "email": "ana@ejemplo.com",
      "phone": null,
      "name": "Ana Torres",
      "status": "pending",
      "signedAt": null,
      "routingOrder": null,
      "delivery": null
    }
  ],
  "hasMore": false,
  "nextCursor": null,
  "previousCursor": null,
  "limit": null
}
```

## 4 Abre una sesión de firma para ese firmante

POST `/signing-sessions` [ver en la referencia](https://allsign.io/developers/docs/endpoints/signing-sessions#create-signing-session)

### Qué haces

allowedOrigins dice qué páginas pueden mostrar la firma. A tu frontend solo le pasas el clientSecret: dura 15 minutos y sirve para una sola sesión, así que pídelo cuando tu usuario vaya a firmar.

### Qué mirar

-   `id` = `"ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a"`
-   `clientSecret` = `"<client-secret>"`
-   `expiresAt` = `"2026-07-11T18:15:01.869000Z"`

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/signing-sessions' \
  -H "Authorization: Bearer $ALLSIGN_API_KEY" \
  -H "AllSign-Version: 2026-07-11" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "documentId": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
  "signerEmail": "ana@ejemplo.com",
  "allowedOrigins": [
    "https://app.ejemplo.com"
  ],
  "successUrl": "https://app.ejemplo.com/firmado",
  "cancelUrl": "https://app.ejemplo.com/cancelado"
}'
```

**Node**

```
import { randomUUID } from 'node:crypto';

const respuesta = await fetch('https://api.allsign.io/v3/signing-sessions', {
    method: 'POST',
    headers: {
        Authorization: `Bearer ${process.env.ALLSIGN_API_KEY}`,
        'AllSign-Version': '2026-07-11',
        'Idempotency-Key': randomUUID(),
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        documentId: 'doc_e6d57f83692e4ff5ad07f57f6bd6a117',
        signerEmail: 'ana@ejemplo.com',
        allowedOrigins: [
            'https://app.ejemplo.com',
        ],
        successUrl: 'https://app.ejemplo.com/firmado',
        cancelUrl: 'https://app.ejemplo.com/cancelado',
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import os
import uuid

import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/signing-sessions",
    headers={
        "Authorization": f"Bearer {os.environ['ALLSIGN_API_KEY']}",
        "AllSign-Version": "2026-07-11",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "documentId": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
        "signerEmail": "ana@ejemplo.com",
        "allowedOrigins": [
            "https://app.ejemplo.com",
        ],
        "successUrl": "https://app.ejemplo.com/firmado",
        "cancelUrl": "https://app.ejemplo.com/cancelado",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 201**

```
{
  "livemode": false,
  "id": "ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a",
  "object": "signing_session",
  "status": "pending",
  "clientSecret": "<client-secret>",
  "document": {
    "id": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
    "title": "Contrato de servicios — Ana Torres"
  },
  "signer": {
    "email": "ana@ejemplo.com",
    "name": "ana@ejemplo.com"
  },
  "expiresAt": "2026-07-11T18:15:01.869000Z",
  "createdAt": null,
  "mountedAt": null,
  "completedAt": null,
  "signature": null,
  "evidence": null
}
```

## 5 AllSign hace el equivalente por ti: revisa la política de orígenes

GET `/signing-sessions/{session_id}/policy` sin API key [ver en la referencia](https://allsign.io/developers/docs/endpoints/signing-sessions#get-session-policy)

### Qué haces

Tu código no llama esta operación. Cuando el SDK abre la firma, AllSign consulta el equivalente desde su servidor y lo convierte en el encabezado Content-Security-Policy: frame-ancestors del iframe, que es lo que hace cumplir el navegador: tu página puede enmarcar la firma y cualquier otra no. Se muestra para que entiendas esa política y para depurar; localhost aparece para que pruebes en tu máquina.

### Qué mirar

-   `allowedOrigins` = `["https://app.ejemplo.com"]`
-   `frameAncestors` = `"https://app.ejemplo.com http://localhost:*"`

### En producción

Si la llamas desde el navegador de tu página, falla en el preflight de CORS con un 400: la API v3 solo acepta peticiones de navegador desde orígenes de AllSign.

**cURL**

```
curl 'https://api.allsign.io/v3/signing-sessions/ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a/policy' \
  -H "AllSign-Version: 2026-07-11"
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/signing-sessions/ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a/policy', {
    headers: {
        'AllSign-Version': '2026-07-11',
    },
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import requests

respuesta = requests.get(
    "https://api.allsign.io/v3/signing-sessions/ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a/policy",
    headers={
        "AllSign-Version": "2026-07-11",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "sessionId": "ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a",
  "allowedOrigins": [
    "https://app.ejemplo.com"
  ],
  "frameAncestors": "https://app.ejemplo.com http://localhost:*"
}
```

## 6 AllSign hace el equivalente por ti: inicia la sesión

POST `/signing-sessions/{session_id}/init` sin API key [ver en la referencia](https://allsign.io/developers/docs/endpoints/signing-sessions#init-signing-session)

### Qué haces

Tampoco la llama tu código. Cuando ejecutas `AllSign.init({ clientSecret })`, el iframe de AllSign inicia la sesión con una operación equivalente que revisa lo mismo que ves aquí: que el clientSecret sea válido y que la página que abre la firma (parentOrigin) esté en tus allowedOrigins. Se muestra para que entiendas qué se revisa y para depurar; en tu frontend basta con `AllSign.init({ clientSecret })` y luego `session.modal()`, `session.inline('#contenedor')` o `session.slider()`.

### Qué mirar

-   `signatureId` = `"sgr_fdcf573b43b24b2484b1a795ffff848f"`
-   `signer` = `{"email":"ana@ejemplo.com","name":"ana@ejemplo.com"}`
-   `document` = `{"id":"doc_e6d57f83692e4ff5ad07f57f6bd6a117","title":"Contrato de servicios — Ana Torres"}`

### En producción

Igual en producción. Llamarla desde el navegador de tu página falla en el preflight de CORS con un 400: de iniciar la sesión se encarga el SDK.

**cURL**

```
curl -X POST 'https://api.allsign.io/v3/signing-sessions/ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a/init' \
  -H "AllSign-Version: 2026-07-11" \
  -H "Content-Type: application/json" \
  -d '{
  "clientSecret": "<client-secret>",
  "parentOrigin": "https://app.ejemplo.com"
}'
```

**Node**

```
const respuesta = await fetch('https://api.allsign.io/v3/signing-sessions/ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a/init', {
    method: 'POST',
    headers: {
        'AllSign-Version': '2026-07-11',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({
        clientSecret: '<client-secret>',
        parentOrigin: 'https://app.ejemplo.com',
    }),
});
console.log(respuesta.status, await respuesta.json());
```

**Python**

```
import requests

respuesta = requests.post(
    "https://api.allsign.io/v3/signing-sessions/ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a/init",
    headers={
        "AllSign-Version": "2026-07-11",
    },
    json={
        "clientSecret": "<client-secret>",
        "parentOrigin": "https://app.ejemplo.com",
    },
)
print(respuesta.status_code, respuesta.json())
```

**Respuesta · 200**

```
{
  "livemode": false,
  "sessionId": "ses_b702eecc5b8d4dd0a6b58d5a5f2c8f5a",
  "signatureId": "sgr_fdcf573b43b24b2484b1a795ffff848f",
  "signerId": "usr_7e3a6c7366b54e50a5f49fbc1f0e8b9c",
  "guestToken": "<guestToken>",
  "signer": {
    "email": "ana@ejemplo.com",
    "name": "ana@ejemplo.com"
  },
  "document": {
    "id": "doc_e6d57f83692e4ff5ad07f57f6bd6a117",
    "title": "Contrato de servicios — Ana Torres"
  },
  "brandProfileId": null,
  "appearance": {
    "primaryColor": "#e54848",
    "primaryTextColor": "#000000",
    "borderRadius": 24,
    "fontFamily": null,
    "poweredByAllSign": true
  },
  "locale": "es",
  "successUrl": "https://app.ejemplo.com/firmado",
  "cancelUrl": "https://app.ejemplo.com/cancelado"
}
```

## Errores comunes de esta receta

Errores reales que el test de esta receta provocó a propósito en sandbox, sobre sus mismos pasos. Cada uno enlaza a su explicación en [Errores](https://allsign.io/developers/docs/errors).

`RESOURCE_NOT_FOUND` · 404 · en el paso 4 (Abre una sesión de firma para ese firmante)

Abre una sesión para alguien que no firma el documento. [Qué significa y qué hacer](https://allsign.io/developers/docs/errors#RESOURCE_NOT_FOUND)

**Respuesta · 404 · RESOURCE_NOT_FOUND**

```
{
  "type": "https://allsign.io/developers/docs/errors#RESOURCE_NOT_FOUND",
  "title": "Not Found",
  "status": 404,
  "detail": "No participant with email nadie@ejemplo.com on document c2af0193-88b8-4767-be86-e9bc40106542",
  "instance": "/v3/signing-sessions",
  "code": "RESOURCE_NOT_FOUND",
  "requestId": "req_243a82a23c1a45aba4757d8ed3758d31"
}
```

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:18 (hora CDMX) · test `docs-checks-v3/tests/recetas/firma-embebida-en-tu-app.receta.spec.ts`

`PERMISSION_DENIED` · 403 · en el paso 6 (AllSign hace el equivalente por ti: inicia la sesión)

Abre la firma en una página que no está en allowedOrigins. [Qué significa y qué hacer](https://allsign.io/developers/docs/errors#PERMISSION_DENIED)

Así responde la API cuando la página que abre la firma no está en allowedOrigins; en tu página lo vas a ver de otra forma. Lo normal es que el navegador ni cargue el iframe y la consola muestre un error de Content-Security-Policy frame-ancestors. Si llega a cargar, la llamada con la que el iframe inicia la sesión responde 403 con el código ORIGIN\_NOT\_ALLOWED. En los dos casos, agrega ese origen exacto (esquema, dominio y puerto) a allowedOrigins al abrir la sesión.

**Respuesta · 403 · PERMISSION_DENIED**

```
{
  "type": "https://allsign.io/developers/docs/errors#PERMISSION_DENIED",
  "title": "Permission denied",
  "status": 403,
  "detail": "The origin 'https://otro.ejemplo.com' is not authorized to embed this session.",
  "instance": "/v3/signing-sessions/ses_ee38d79b09ad45b9ad094034c1910ecc/init",
  "code": "PERMISSION_DENIED",
  "requestId": "req_beaface9931b45e4a4eb9e51c8b13f9c"
}
```

Grabado en el entorno interno de AllSign con una key de sandbox · backend `24c0469` · 8 oct 2026, 06:18 (hora CDMX) · test `docs-checks-v3/tests/recetas/firma-embebida-en-tu-app.receta.spec.ts`

## Siguiente

-   [Recibe y verifica los webhooks de AllSign](https://allsign.io/developers/docs/recetas/recibe-y-verifica-webhooks) — Un endpoint que recibe los eventos de AllSign, comprueba que de verdad los firmó AllSign y sobrevive a sus propias caídas y a la rotación del secreto.
