Metadata-Version: 2.5
Name: easycontract
Version: 0.1.1
Summary: SDK oficial de Python para el API de easycontract (firma electrónica eIDAS)
Project-URL: Documentation, https://easycontract.online/docs/api
Author-email: Román Ramírez <roman@easycontract.online>
License-Expression: LicenseRef-easycontract-proprietary
License-File: LICENSE.txt
Keywords: audit-trail,contract,e-signature,eidas,esign,webhooks
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: requests<3,>=2.33.0
Description-Content-Type: text/markdown

# easycontract — SDK de Python

SDK oficial del API de firma electrónica de easycontract (SES/AdES bajo eIDAS,
con paquete de evidencias defendible en litigio).

```bash
pip install easycontract
```

```python
from easycontract import EasyContract

client = EasyContract(api_key="ec_test_...")  # ec_live_... en producción
print(client.whoami())
```

- **Modo test de primera clase**: una clave `ec_test_…` opera sobre datos
  aislados (TSA/PAdES simulados). Nadie debería firmar un documento real para
  probar la integración.
- **Idempotencia automática**: todo POST lleva `Idempotency-Key` (uuid4);
  pásala tú misma para reintentos seguros de tu lado.
- **Reintentos**: 429 (respetando `Retry-After`) y 5xx en GETs.
- **Errores tipados** con el `code` estable del API:
  `AuthenticationError`, `RateLimitError`, `ForbiddenError` (cuota/feature/
  regla de membresía), `InvalidRequestError`, `NotFoundError`,
  `ConflictError`, `ServerError`.

---

## Receta 1 — Enviar un envelope desde plantilla (el flujo RootedCON)

Una sola llamada: cubre los roles, envía, y recibe los enlaces de firma.

```python
result = client.templates.create_envelope(
    template_id,
    signers={"ponente": {"full_name": "Ada Lovelace", "email": "ada@example.com"}},
    title="Contrato ponencia RootedCON 2027 — Ada Lovelace",
    send=True,  # congela hashes, plan y retención, y acuña enlaces
    deliver_emails=True,  # easycontract envía los emails con los enlaces
)
envelope_id = result["envelope"]["id"]
```

Creación de la plantilla (una vez):

```python
template = client.templates.create(name="Contrato ponente", retention_period_months=72)
doc = client.templates.add_document(template["id"], file="contrato-ponente.pdf")
client.templates.add_role(template["id"], key="ponente", label="Ponente")
client.templates.add_field(
    template["id"],
    document_id=doc["id"],
    role="ponente",
    anchor_text="Fdo. el ponente:",  # la firma se estampa donde el PDF lo dice
)
```

## Receta 2 — Firma embebida (embedded signing)

El firmante firma dentro de tu aplicación (el caso RootedArbiter). Tres pasos:

1. Registra una vez los orígenes de tu web (https; `http://localhost:…` solo
   en desarrollo). La lista se sustituye entera:

```python
client.organization.update(embed_allowed_origins=["https://arbiter.rootedcon.com"])
```

2. Envía sin emails y, cuando el ponente esté delante de tu página, acuña una
   sesión efímera para ESE firmante (60 minutos por defecto, caduca al firmar):

```python
result = client.templates.create_envelope(
    template_id,
    signers={...},
    send=True,
    deliver_emails=False,
)
signer = result["signers"][0]
session = client.envelopes.create_embedded_session(
    result["envelope"]["id"],
    signer["id"],
    origin="https://arbiter.rootedcon.com",
    return_url="https://arbiter.rootedcon.com/contratos/42/firmado",  # opcional
    language="es",
)
embed_url = session["url"]  # token em_…: un firmante, un origen, caduca
```

3. Ábrela en un iframe y escucha los eventos, o redirige al ponente y recíbelo
   de vuelta en `return_url` con `?easycontract_event=signed&envelope_id=…&signer_id=…`:

```html
<iframe src="{{ embed_url }}" allow="camera" style="width:100%;height:80vh;border:0"></iframe>
<script>
window.addEventListener("message", (e) => {
  if (e.origin !== "https://app.easycontract.online") return;   // el origen de easycontract
  if (e.data?.source !== "easycontract") return;
  // e.data.event ∈ ready | signed | declined | expired; e.data.envelope_id, signer_id
});
</script>
```

La página de firma responde con `Content-Security-Policy: frame-ancestors
<tu origen>` y no depende de cookies (funciona con las de terceros
bloqueadas); el OTP y la verificación de identidad, si el firmante los exige,
se piden dentro del mismo marco (`allow="camera"` para la captura del
documento). El `postMessage` es solo señal de interfaz: confirma siempre en
servidor con el webhook `signer.signed` o `client.envelopes.get(...)`. Cada
sesión y su apertura (con el origen) quedan en el expediente de evidencias.

El `sign_url` clásico de `deliver_emails=False` sigue existiendo para el
redirect simple (14 días, `expires_in_days` para cambiarlo).

## Receta 3 — Webhooks vs. polling

**Webhooks** (recomendado): registra un endpoint y verifica CADA entrega.

```python
endpoint = client.webhooks.create(url="https://miapp.com/hooks/easycontract")
WEBHOOK_SECRET = endpoint["secret"]  # whsec_… — se muestra UNA sola vez

# En tu receptor (Django/Flask/FastAPI):
from easycontract import webhooks

event = webhooks.verify_signature(
    payload=request.body,
    header=request.headers["X-EasyContract-Signature"],
    secret=WEBHOOK_SECRET,
)
if event["type"] == "envelope.completed":
    ...
```

**Reconciliación / polling**: los webhooks son la proyección de un event log
consultable; si pierdes una entrega, reconcilia:

```python
for event in client.events.auto_paging_iter(type="envelope.completed"):
    ...
```

## Receta 4 — Descargar el paquete de evidencias

El ZIP completo (documentos, certificado, audit trail JSON verificable
offline, sellos TSA) — lo que un tenant llevaría a un juzgado:

```python
client.envelopes.download_evidence(envelope_id, to="evidencias.zip")
client.envelopes.download_certificate(envelope_id, to="certificado.pdf")
```

## Receta 5 — Gestión de plantillas

```python
for template in client.templates.list()["data"]:
    print(template["name"])

detail = client.templates.get(template_id, expand=["documents", "roles", "fields"])
client.templates.delete(old_template_id)
```

---

## Envelope manual (sin plantilla)

```python
envelope = client.envelopes.create(
    title="NDA",
    retention_period_months=60,  # retención SIEMPRE explícita
)
client.envelopes.add_document(envelope["id"], file="nda.pdf")
client.envelopes.add_signer(
    envelope["id"],
    full_name="Grace Hopper",
    email="grace@example.com",
    auth_methods=["email_link", "sms_otp"],
    phone="+34600111222",  # OTP: plan Pro+
)
sent = client.envelopes.send(envelope["id"])
```

## Organización, miembros e invitaciones

```python
org = client.organization.get()  # plan, límites y features del plan
members = client.members.list()
invite = client.invitations.create(email="colega@example.com", role="member")
# invite["accept_url"] — entrégalo tú o deja que easycontract lo envíe por email
```

## Validar la identidad del firmante en tu plataforma (KYC)

Con el proceso propio de easycontract, el firmante fotografía su documento y se hace un
selfie; la decisión es de la organización. Puedes tomarla desde tu propio sistema:

```python
from easycontract import EasyContract

client = EasyContract(api_key="ec_live_...")

# 1. Suscríbete a los eventos (o sondea GET /events/): signer.kyc_submitted avisa de
#    cada caso pendiente; signer.kyc_verified / signer.kyc_failed, de la decisión.
for case in client.identity.pending_reviews()["data"]:
    envelope_id, signer = case["envelope_id"], case["signer"]
    identity = case["identity_verification"]  # estado, comprobaciones, datos declarados

    # 2. Recupera las imágenes (cada descarga queda en el audit trail del documento)
    for document in identity["documents"]:
        client.identity.download_document(
            envelope_id, signer["id"], document["id"], to=f"/tmp/{document['kind']}.jpg"
        )

    # 3. Valida con tus reglas: número declarado, caducidad, cotejo visual o tu propio motor
    ok = identity["declared_document_number"] == mi_registro[signer["email"]].dni

    # 4. Decide; el firmante recibe el resultado por correo
    client.identity.review(
        envelope_id,
        signer["id"],
        decision="approve" if ok else "reject",
        verified_name=signer["full_name"],
        reason="" if ok else "El número de documento no coincide con nuestro registro",
    )
```

Las imágenes se purgan a los N días de la decisión (`organization.kyc.image_retention_days`);
el resultado completo sigue disponible en el paquete de evidencias con
`download_evidence(..., include_identity_proofs=True)`.
