# API y webhooks

Crea, envía, sigue y descarga solicitudes de firma desde tu propio software, y recibe avisos cuando cambian.

La API de eSign permite que tu propio software envíe PDF a firmar y descargue el resultado sellado. eSign avisa a tu
software (un webhook) cada vez que una solicitud cambia.

## Dirección y clave

Todas las llamadas van a tu dirección de eSign seguida de `/api/v1`, por ejemplo
`https://esign.sapiensandmachines.com/api/v1`, y llevan la clave de API de tu espacio:

```http
Authorization: Bearer YOUR_API_KEY
Accept: application/json
```

Las claves de API pertenecen al espacio, no a una persona, y no vencen. S+M las crea por ti; cada una se muestra una
sola vez. Pídenos revocar una clave que ya no uses. Hasta 120 llamadas por minuto.

## Envía un documento

`POST /envelopes` con el PDF y las personas que firman. Agrega `send=1` para enviarlo de inmediato. En este ejemplo
la clave está en la variable de entorno `ESIGN_API_KEY`.

```bash
curl -X POST https://esign.sapiensandmachines.com/api/v1/envelopes \
  -H "Authorization: Bearer $ESIGN_API_KEY" -H "Accept: application/json" \
  -F title="Contrato de servicio" \
  -F document=@contrato.pdf \
  -F "recipients[0][key]=cliente" \
  -F "recipients[0][name]=Ana García" \
  -F "recipients[0][email]=ana@example.com" \
  -F "recipients[0][role]=signer" \
  -F locale=es \
  -F send=1
```

| Campo | |
|---|---|
| `title` | Obligatorio, hasta 255 caracteres. |
| `document` o `document_base64` | El PDF (hasta 25 MB, sin contraseña). |
| `recipients[]` | De 1 a 20 personas: `key` (tu propio nombre para ella), `name`, `email`, `role` (`signer` o `cc`), `routing_order` (1–20; el mismo número firma a la vez), `verification` (`none` o `email_code` para un código de 6 dígitos por correo). |
| `message` | Se muestra a los firmantes, hasta 5.000 caracteres. |
| `locale` | `en` o `es`; si no, el idioma de los firmantes de tu espacio. |
| `expires_in_days` | De 1 a 365; 30 si no se indica. |
| `external_ref` | Tu propia referencia, para encontrar la solicitud después. |
| `callback_url` | Adónde enviar los webhooks de esta solicitud, en lugar de la dirección de tu espacio. |
| `send` | `true` para enviar ahora; si no, queda como borrador. |
| `fields[]` | Opcional: coloca los campos por posición en lugar de con [marcadores](https://docs.sapiensandmachines.com/es/esign/plantillas-y-marcadores). |

Los marcadores del PDF deciden dónde firma cada firmante: `@2` en un marcador es el segundo firmante de `recipients`.

## Otras llamadas

| Llamada | Hace |
|---|---|
| `GET /envelopes` | Lista las solicitudes; filtra por `status` o `external_ref`; `per_page` hasta 100. |
| `GET /envelopes/{id}` | Una solicitud, con el estado de cada destinatario. |
| `POST /envelopes/{id}/send` | Envía un borrador. |
| `POST /envelopes/{id}/void` | La cancela (`reason`); no una vez completada. |
| `GET /envelopes/{id}/document` | El PDF sellado una vez completada (`409` antes); su SHA-256 va en el encabezado `X-Document-Sha256`. |
| `GET /envelopes/{id}/audit` | El historial, y si su cadena de registros está intacta (`chain_intact`). |

El `status` de una solicitud es `draft`, `sent`, `completed`, `declined`, `voided` o `expired`. El estado de cada
destinatario indica si ya vio o firmó.

## Webhooks

eSign envía un `POST` con JSON al `callback_url` de la solicitud o, si no hay, a la dirección de webhooks de tu
espacio, para:

| Evento | Cuándo |
|---|---|
| `recipient.viewed` | Un firmante abrió el documento. |
| `recipient.signed` | Un firmante firmó. |
| `envelope.completed` | Todos firmaron; el PDF sellado está listo. |
| `envelope.declined` | Un firmante lo rechazó. |
| `envelope.expired` | La solicitud venció. |
| `envelope.voided` | Se canceló. |

```json
{
  "event": "envelope.completed",
  "occurred_at": "2026-10-15T15:04:05Z",
  "recipient": null,
  "envelope": {
    "id": "…",
    "status": "completed",
    "title": "Contrato de servicio",
    "external_ref": "COT-1042",
    "sealed_sha256": "…",
    "completed_at": "2026-10-15T15:04:05Z",
    "document_url": "…"
  }
}
```

En los eventos de destinatario, `recipient` incluye su `id`, `name`, `email`, `status`, `viewed_at`, `signed_at` y,
si rechazó, `decline_reason`.

Responde con cualquier estado `2xx`. Si no, eSign lo reintenta a los 10 segundos, 1 minuto, 5 minutos, 15 minutos y
1 hora.

### Comprueba que un webhook viene de eSign

Cada webhook lleva `X-Esign-Event` y `X-Esign-Signature: sha256=…`, un HMAC-SHA256 del cuerpo sin modificar con tu
secreto de webhooks. Calcúlalo y compáralo antes de confiar en el contenido:

```php
$body = file_get_contents('php://input');
$expected = 'sha256='.hash_hmac('sha256', $body, $webhookSecret);
if (! hash_equals($expected, $_SERVER['HTTP_X_ESIGN_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}
```

## Errores

Los errores vuelven como JSON con un estado HTTP: `401` si falta la clave o es incorrecta, `403` si el espacio está
suspendido, `404` si la solicitud no existe, `409` si pides el documento antes de que esté completado, `422` con los motivos
cuando algo de la solicitud está mal (por ejemplo, un marcador para un firmante que no se indicó, o enviar una
solicitud que no es un borrador) y `429` si hay demasiadas llamadas.
