Metadata-Version: 2.4
Name: secure-exchanges-sdk
Version: 2026.8.26
Summary: Python SDK for Secure Exchanges - Secure file transfer and messaging
Author-email: Secure Exchanges <supportdev@secure-exchanges.info>
License-Expression: Apache-2.0
Project-URL: Homepage, https://www.secure-exchanges.com
Keywords: secure-exchanges,encryption,secure-messaging,file-transfer,aes-256,post-quantum,ml-kem,cryptography
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: Programming Language :: Python :: 3.14
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: cryptography>=48.0.0
Requires-Dist: phonenumbers>=8.13.0
Requires-Dist: typing-extensions>=4.0.0
Provides-Extra: signature
Requires-Dist: pypdf>=4.0.0; extra == "signature"
Requires-Dist: playwright>=1.40.0; extra == "signature"
Provides-Extra: test
Requires-Dist: secure-exchanges-sdk[signature]; extra == "test"
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
Provides-Extra: dev
Requires-Dist: secure-exchanges-sdk[test]; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# Secure Exchanges Python SDK

**English** | [Français](#secure-exchanges-python-sdk-version-française)

Python SDK for the Secure Exchanges API: secure file transfer, end-to-end encrypted messaging, and digital signing of PDF documents.

The SDK covers the entire protocol: post-quantum handshake, end-to-end encryption of messages and files, electronic signatures with PKI certification, and message logs.

---

## Table of contents

1. [Installation](#installation)
2. [Prerequisites](#prerequisites)
3. [Credentials and configuration](#credentials-and-configuration)
   - [Where to find your credentials](#where-to-find-your-credentials)
   - [Configuration](#configuration)
   - [Protecting your credentials](#protecting-your-credentials)
4. [Security](#security)
5. [SDK architecture](#sdk-architecture)
6. [Usage guide](#usage-guide)
   - [Sending a simple message](#sending-a-simple-message)
   - [Sending with password and SMS code (2FA)](#sending-with-password-and-sms-code-2fa)
   - [Sending with attachments](#sending-with-attachments)
   - [Sending with PKI certification](#sending-with-pki-certification)
   - [Retrieving a message](#retrieving-a-message)
   - [Message logs](#message-logs)
   - [Deleting a message](#deleting-a-message)
   - [Reply envelopes](#reply-envelopes)
7. [API reference](#api-reference)
   - [MessageHelper](#messagehelper)
   - [LogsHelper](#logshelper)
   - [SignHelper](#signhelper)
   - [PkiFileHelper](#pkifilehelper)
   - [ZoneBuilder](#zonebuilder)
   - [CryptoHelper](#cryptohelper)
8. [Data models](#data-models)
9. [Support](#support)
10. [License](#license)

---

## Installation

```bash
pip install secure-exchanges-sdk
```

This installs everything needed for sending messages, attachments,
logs, and phone number validation.

### Electronic signature on PDF (`signature` extra)

Defining signature zones (`ZoneBuilder`) and PDF certification
(`PkiFileHelper`) require two additional dependencies, `pypdf` and
`playwright`, deliberately not installed by default: `playwright` downloads a
browser of roughly 150 MB:

```bash
pip install "secure-exchanges-sdk[signature]"
playwright install chromium
```

---

## Prerequisites

To use the SDK, you need:

- **Python 3.10+**
- **A serial number** (`serial`) : the UUID of your licence
- **An API user ID** (`api_user`) : the UUID of the API user
- **An API password** (`api_password`) : the UUID of the API password

---

## Credentials and configuration

### Where to find your credentials

The 3 required credentials (`serial`, `api_user`, `api_password`) are UUIDs (GUIDs) that you obtain from your **Secure Exchanges administration portal** when creating your API licence.

| Credential | Description | Example |
|------------|-------------|---------|
| `serial` | Serial number of your licence | `UUID("d17ca972-5f70-4961-bac4-98e6b91b1dc4")` |
| `api_user` | API user identifier | `UUID("0fa0cf77-4ca5-4406-8623-1ed0ffd47290")` |
| `api_password` | API user password | `UUID("4bfeabdf-c498-448a-9255-1e17c3ef8c0b")` |

### Configuration

`SettingsHelper` selects the environment and returns its endpoints, nothing
else. The SDK stores no credentials and never reads them from the process
environment: the serial, the API user and the API password are yours, and are
passed explicitly to every call.

```python
from secure_exchanges_sdk.client import SecureExchangesClient, SEMSClient
from secure_exchanges_sdk.helpers.settings_helper import SettingsHelper

cfg = SettingsHelper.configure_production()

client      = SecureExchangesClient(cfg['api_endpoint'])
sems_client = SEMSClient(cfg['sems_endpoint'])

# load_my_credentials() is your own function: it returns the three UUID objects
# of your licence (uuid.UUID, not str). See "Protecting your credentials"
# below for where to keep them.
serial, api_user, api_password = load_my_credentials()
```

| Function | Environment | Endpoints |
|----------|-------------|-----------|
| `configure_production()` | production | `www.secure-exchanges.com` |
| `configure_preview()` | preview (test) | `preview.secure-exchanges.com` |

Each one returns a dict with `api_endpoint`, `sems_endpoint`, `file_handler`,
`file_upload_handler` and `environment`.

> Preview and production credentials are different: a production serial will
> not work in preview, and vice versa.

### Protecting your credentials

These three UUIDs are the equivalent of a password on your Secure Exchanges
account: whoever holds them can send, read and delete messages in your name,
and the usage is billed to you. How they are stored is entirely your
decision, and your responsibility.

- **Never commit them.** Keep them out of the repository, and check the history
  too: `git log -S <serial>` finds a secret committed once and removed later.
  A secret that reached a remote is compromised: rotate it, don't just delete
  the line.
- **Never hard-code them in source.** They end up in stack traces, in bug
  reports, in the packages you ship, and in every fork of the code.
- **Prefer a secret manager.** Encrypted at rest, access audited, rotation
  possible without a redeploy.
- **If you keep them in a file, encrypt it or lock it down.** Outside the web
  root, owned by the service account, permissions `0400`, and excluded from
  your backups or encrypted inside them.
- **Beware of the process environment.** Environment variables are readable
  from `/proc/<pid>/environ` by anything running under the same user, they show
  up in crash dumps and in most error reporters. Load the credentials at the
  moment you need them rather than exporting them for the whole process
  lifetime.
- **Keep them out of your logs.** Exclude them explicitly from your error
  handler, from your crash reporter and from request dumps, which capture
  environment and local variables by default.
- **Rotate them from the administration portal** the moment you suspect an
  exposure: a laptop lost, a log shipped to a third party, a developer leaving.

---

## Security

- **End-to-end encryption**: the content (message body and files) is
  encrypted locally, with keys generated on your machine and **never
  sent to the server**: the server only stores and relays encrypted
  data.
- **Post-quantum key exchange**: **ML-KEM-1024** (FIPS 203), resistant to
  quantum computer attacks, signed with **ML-DSA-87** (FIPS 204).
- **AES-256 encryption** of the content and of all exchanges with the server.
- **Interception protection**: any man-in-the-middle (MITM) attack attempt
  causes the connection to fail.
- **Nothing to manage**: the SDK automatically applies the entire protocol
  on every call: you never have to handle any cryptographic primitives.

---

## SDK architecture

Overview of the main modules. In practice, you will mostly use the
*helpers*, which orchestrate the full protocol for you:

| Module | Role |
|--------|------|
| `client/` | HTTP clients: `SecureExchangesClient` (main API), `SEMSClient` (email delivery) |
| `helpers/message_helper.py` | Sending, reading, deleting messages, reply envelopes |
| `helpers/handshake_helper.py` | Post-quantum handshake (ML-KEM-1024 + ML-DSA-87) |
| `helpers/post_quantum_crypto_helper.py` | Post-quantum primitives for the handshake |
| `helpers/crypto_helper.py` | Low-level symmetric/RSA cryptography (AES-256, SHA-512, .NET GUIDs) |
| `helpers/logs_helper.py` | Retrieval of message logs |
| `helpers/sign_helper.py`, `helpers/zone_builder.py` | Electronic signature zones on PDF |
| `helpers/pki_file_helper.py` | PKI certification of PDFs (headless browser) |
| `helpers/file_helper.py` | Encrypted file upload/download (chunked, parallel) |
| `helpers/settings_helper.py` | Configuration (environments, endpoints) |
| `helpers/contact_helper.py`, `licence_helper.py` | Contacts, licence validation |
| `helpers/email_helper.py`, `phone_helper.py`, `mime_helper.py` | Validations (email, phone, MIME types) |
| `models/` | Data models: responses (`answer/`), entities (`entity/`), enumerations (`enum/`), transport, JSON |
| `callback/` | Models for notification callbacks |

---

## Usage guide

All the examples below assume the following setup (see
[Configuration](#configuration)):

```python
from secure_exchanges_sdk.client import SecureExchangesClient, SEMSClient
from secure_exchanges_sdk.helpers.settings_helper import SettingsHelper

cfg = SettingsHelper.configure_production()

client      = SecureExchangesClient(cfg['api_endpoint'])
sems_client = SEMSClient(cfg['sems_endpoint'])

# Your own function: it returns the three UUID objects of your licence.
serial, api_user, api_password = load_my_credentials()
```

### Sending a simple message

```python
from secure_exchanges_sdk.helpers.message_helper import MessageHelper
from secure_exchanges_sdk.models.entity.recipient_info import RecipientInfo

recipients = [
    RecipientInfo(email="recipient@example.com")
]

answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Hello, here is a secure message.</p>",
    subject="Confidential message",
    culture_id="en-US"
)

if answer.status == 200:
    print("Message sent successfully!")
    for ra in answer.recipients_answer:
        if ra.answer and ra.answer.url:
            print(f"  URL: {ra.answer.url}")
else:
    print(f"Error {answer.status}: {answer.data}")
```

### Sending with password and SMS code (2FA)

```python
from secure_exchanges_sdk.models.enum.send_method_enum import SendMethodEnum

recipients = [
    RecipientInfo(email="user@example.com", phone="+15145551234")
]

answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Confidential document attached.</p>",
    subject="Protected document",
    password="SecretPassword123",
    send_method=SendMethodEnum.EMAIL_WITH_SMS_CODE,  # link by email, code by SMS
    culture_id="en-US",
    maximum_open_time=3,    # Maximum number of opens (1-99)
    minutes_expiration=1440 # Expiration in minutes (1440 = 24h)
)
```

`SendMethodEnum` values:

| Value | Description |
|-------|-------------|
| `ONLY_EMAIL` (0) | Link sent by email only (default) |
| `SMS_ONLY` (1) | Link sent by SMS only |
| `SMS_WITH_EMAIL_CODE` (2) | Link by SMS, opening code by email |
| `EMAIL_WITH_SMS_CODE` (3) | Link by email, opening code by SMS |

> Password protection (`password`) is independent of the delivery channel
> and can be combined with any `send_method` value.

### Sending with attachments

```python
# From disk
answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>See attached files.</p>",
    subject="Secure files",
    files_path=["/path/to/document.pdf", "/path/to/image.png"]
)

# From memory (bytes)
pdf_bytes = open("/path/to/report.pdf", "rb").read()
csv_bytes = open("/path/to/data.csv", "rb").read()

files_in_memory = [
    {"binary": pdf_bytes, "file_name": "report.pdf"},
    {"binary": csv_bytes, "file_name": "data.csv"}
]

answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Report and data attached.</p>",
    subject="Monthly report",
    files_list=files_in_memory
)
```

### Sending with PKI certification

PKI certification applies a digital certificate to a PDF before sending it for signature. The process uses a headless browser (Playwright, `signature` extra). Signature zones are defined with `ZoneBuilder`, which reads the actual dimensions of the PDF pages:

```python
import asyncio
from secure_exchanges_sdk.helpers.crypto_helper import CryptoHelper
from secure_exchanges_sdk.helpers.zone_builder import ZoneBuilder
from secure_exchanges_sdk.helpers.sign_helper import SignHelper
from secure_exchanges_sdk.helpers.pki_file_helper import PkiFileHelper, CertifyPdfArgs
from secure_exchanges_sdk.models.entity.sign_zone_definition import SignZoneDefinition
from secure_exchanges_sdk.models.transport.file_zone_definition import FileZoneDefinition
from secure_exchanges_sdk.models.transport.recipient_zone_definition import RecipientZoneDefinition

# 1. Read the PDF and compute its SHA512
pdf_path = "contract.pdf"
with open(pdf_path, "rb") as f:
    pdf_bytes = f.read()
sha512 = CryptoHelper.get_sha512_hash_of_bytes(pdf_bytes)

# 2. Define the signature zones with ZoneBuilder
with ZoneBuilder(pdf_path, is_pki=True) as zone_builder:
    zones = [
        # Signature zone on page 1
        zone_builder.create_field_sign_zone(
            page=1, field_name="signature1",
            x=60, y=400, width=200, height=50
        ),
        # Date zone filled automatically at signing time
        zone_builder.create_field_date_sign_zone(
            page=1, field_name="date1",
            x=60, y=500, width=100, height=25, font_size=12
        ),
    ]

# 3. Associate the zones with the recipient (file referenced by its SHA512)
recipient_zones = [
    RecipientZoneDefinition(
        email="signer@example.com",
        phone=None,
        zones_def_by_file=[
            FileZoneDefinition(
                unique_name=sha512,
                do_not_encrypt_pdf=False,
                do_not_append_certificate_to_file=False,
                is_pki_file=True,
                zones_def=SignZoneDefinition(mode="define", zones=zones)
            )
        ]
    )
]

# 4. Get the certification token
cert_response = SignHelper.get_certification_token(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    original_hash=sha512,
    culture_id="en-US",
    token_type="CertifyDocument"
)
cert_data = cert_response["certification_data"]

# 5. Certify the PDF via headless browser
certify_args = CertifyPdfArgs(
    file_bytes=pdf_bytes,
    file_name="contract.pdf",
    recipients=["signer@example.com"],
    certify_data=cert_data,
    detect_existing_fields=True,
    recipient_zone_definitions=recipient_zones
)

async def certify():
    async with PkiFileHelper(is_preview=False, culture="en-US") as helper:
        return await helper.certify_pki_pdf(certify_args)

certified = asyncio.run(certify())

# 6. Send the certified PDF
answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=[RecipientInfo(email="signer@example.com")],
    message="<p>Please sign this certified document.</p>",
    subject="PKI document to sign",
    certified_pdfs=[certified],
    owner_dont_need_to_sign=True,
    sems_client=sems_client
)
```

A complete, runnable example, including the **signature options** (`SignOptions`: signature mode restriction, requesting a file in return) and the **callback parameters** (contextual keys), can be found in `example_pki_with_signature_options.py` at the root of `pythonSDK/`.

### Retrieving a message

```python
# Parse a Secure Exchanges link
link = "https://www.secure-exchanges.com/message.aspx?msgid=xxx&sems=yyy&cpart=zzz"
msg_params = MessageHelper.get_secure_exchanges_message_from_link(link)

# Retrieve and decrypt the message
response = MessageHelper.get_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    msg_params=msg_params,
    password="MyPassword"     # if protected
)
```

> **Note**: each read consumes one of the message's allowed opens
> (`maximum_open_time`).

### Message logs

Track the lifecycle of your messages (sending, opens, signatures):

```python
from datetime import datetime, timedelta
from uuid import UUID
from secure_exchanges_sdk.helpers.logs_helper import LogsHelper

# All logs from the last 60 days
logs_response = LogsHelper.get_logs(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    from_date=datetime.now() - timedelta(days=60),
    to_date=datetime.now()
)

# The log of a specific message (by message_id OR tracking_id)
log_response = LogsHelper.get_log(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    tracking_id=UUID("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
)
```

### Deleting a message

```python
from uuid import UUID

success = MessageHelper.delete_message_by_tracking_id(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    tracking_id=UUID("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
)
```

### Reply envelopes

Envelopes let a recipient reply to you securely:

```python
# Single envelope (one recipient)
env_response = MessageHelper.get_enveloppe(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    subject="Send me your documents",
    destination="recipient@example.com",
    culture_id="en-US",
    reply_expiration_hours=48,
    maximum_reply_open_time=3,
    reply_to_api=True,
    authorized_extensions=[".pdf", ".docx"]
)

if env_response.url:
    print(f"Envelope URL: {env_response.url}")

# Multiple envelopes
env_responses = MessageHelper.get_envelopes(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    subject="Required documents",
    recipients=[
        {"Email": "user1@example.com"},
        {"Email": "user2@example.com"}
    ]
)
```

---

## API reference

### MessageHelper

Static class for sending and receiving messages.

| Method | Description |
|--------|-------------|
| `multi_recipient_message(api_client, serial, api_user, api_password, recipients, message, subject, ...)` | Sends a message to multiple recipients |
| `get_message(api_client, serial, api_user, api_password, msg_params, password=None, digit_code=None, sems_client=None)` | Retrieves and decrypts a message |
| `delete_message_by_tracking_id(api_client, serial, api_user, api_password, tracking_id, ...)` | Deletes a message by tracking ID |
| `get_enveloppe(api_client, serial, api_user, api_password, subject, destination, ...)` | Creates a reply envelope |
| `get_envelopes(api_client, serial, api_user, api_password, subject, recipients, ...)` | Creates multiple envelopes |
| `get_secure_exchanges_message_from_link(link)` | Parses an SE link into parameters |
| `send_trace(api_client, trace_content)` | Sends a support trace |

#### Parameters of `multi_recipient_message`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_client` | `SecureExchangesClient` | required | API client |
| `serial` | `UUID` | required | Serial number |
| `api_user` | `UUID` | required | API user ID |
| `api_password` | `UUID` | required | API password |
| `recipients` | `List[RecipientInfo]` | required | Recipients |
| `message` | `str` | required | HTML content of the message |
| `subject` | `str` | required | Message subject |
| `password` | `str` | `None` | Protection password |
| `files_list` | `List[dict]` | `None` | In-memory files (`{"binary": bytes, "file_name": str}`) |
| `files_path` | `List[str]` | `None` | Paths of files on disk |
| `send_method` | `SendMethodEnum` | `ONLY_EMAIL` | Delivery channel (see the `SendMethodEnum` table above) |
| `get_back_html` | `bool` | `True` | `True`=you send the email, `False`=SEMS sends it |
| `show_subject` | `bool` | `True` | Show the subject in the notification |
| `get_notify` | `bool` | `True` | Notify on open |
| `culture_id` | `str` | `"en-US"` | Language (`"fr-CA"`, `"en-US"`) |
| `maximum_open_time` | `int` | `1` | Maximum number of opens (1-99) |
| `minutes_expiration` | `int` | `10080` | Expiration in minutes (default: 7 days) |
| `create_message_copy` | `bool` | `False` | Copy for the sender |
| `name` | `str` | `"Secure Exchanges"` | Sender name |
| `callback_parameters` | `str` | `None` | Free-form parameters (JSON) returned in notification callbacks |
| `reply_options` | `dict` | `None` | Recipient reply options (see below) |
| `owner_dont_need_to_sign` | `bool` | `True` | The sender does not sign |
| `certified_pdfs` | `List[CertifiedPdfContainer]` | `None` | PKI-certified PDFs |
| `sign_options` | `SignOptions` | `None` | Signature options: mode restriction, CC/BCC recipients, upload options |
| `sems_client` | `SEMSClient` | `None` | SEMS client (created automatically if omitted) |

`reply_options` is a dictionary (PascalCase keys, matching the API) that
controls what the recipient can do in reply:

```python
from secure_exchanges_sdk.models.enum.reply_file_upload_mode import ReplyFileUploadMode

reply_options = {
    "UploadMode": int(ReplyFileUploadMode.UPLOAD_MANDATORY),  # NO_UPLOAD, UPLOAD_OPTIONAL, UPLOAD_MANDATORY
    "AuthorizedExtensions": [".jpg", ".png", ".pdf"],
    "Messages": [
        {"CultureID": "fr-CA", "Message": "Veuillez joindre votre document."},
        {"CultureID": "en-CA", "Message": "Please attach your document."},
    ],
    "NoEditor": False,   # disable the reply text editor
    "NoReply": False,    # disallow any reply
}
```

`sign_options` (`SignOptions`, in `models/json_models/sign_options.py`)
provides, for a signature send: `signature_restriction`
(`SignatureModeRestriction`), `cc_recipients` / `bcc_recipients`
(carbon-copy recipients, non-signers) and `upload_file_options`.

### LogsHelper

Retrieval of message logs (sends, opens, activities).

| Method | Description |
|--------|-------------|
| `get_logs(api_client, serial, api_user, api_password, from_date, to_date, user_log_serial=None)` | All logs over a date range (`datetime`) → `GetLogsResponse` |
| `get_log(api_client, serial, api_user, api_password, message_id=None, tracking_id=None, user_log_serial=None)` | Log of a specific message: provide `message_id` **or** `tracking_id` → `GetLogResponse` |

### SignHelper

Management of digital signature zones on PDFs.

| Method | Description |
|--------|-------------|
| `add_file_zone_def_to_recipient_index(recipient_index, template_zones, sha512, recipients_match)` | Converts templates into zone definitions |
| `convert_recipient_index_to_list(recipient_index)` | Converts the index into a list for `multi_recipient_message` |
| `concat_recipient_zone_definition_lists(list1, list2)` | Merges two lists of zone definitions |
| `validate_all_recipients_have_zones(files_to_sign, recipients, recipients_zone_def)` | Checks that all recipients have zones |
| `validate_document_signature_integrity(api_client, file_sha512)` | Validates the integrity of a signed document |
| `get_certification_token(api_client, serial, api_user, api_password, original_hash, culture_id, ...)` | Obtains a PKI certification token |

### PkiFileHelper

PKI certification of PDFs via headless browser (Playwright, `signature` extra).

```python
# Asynchronous (recommended)
async with PkiFileHelper(is_preview=True, culture="en-US") as helper:
    result = await helper.certify_pki_pdf(args)

# Synchronous
helper = PkiFileHelper(is_preview=True)
result = helper.certify_pki_pdf_sync(args)
helper.close_sync()
```

#### CertifyPdfArgs

| Property | Type | Description |
|----------|------|-------------|
| `file_bytes` | `bytes` | Binary content of the PDF |
| `file_name` | `str` | File name |
| `recipients` | `List[str]` | Recipient email addresses |
| `certify_data` | `dict` | Certification token (from `get_certification_token`) |
| `detect_existing_fields` | `bool` | Detect existing fields (default: `True`) |
| `recipient_zone_definitions` | `List[RecipientZoneDefinition]` | Predefined zones |
| `define_recipients` | `Callable` | Callback for distributing zones among recipients |

#### CertifiedPdfContainer

| Property | Type | Description |
|----------|------|-------------|
| `file_bytes` | `bytes` | Certified PDF |
| `file_name` | `str` | File name |
| `sign_file_def` | `dict` | Signature definition (`SHA512`, `IsPKIFile`, etc.) |
| `recipient_zone_definitions` | `List[RecipientZoneDefinition]` | Zones updated with the new SHA512 |

### ZoneBuilder

Builds signature zones from a PDF (`helpers/zone_builder.py`,
`signature` extra). The builder reads the actual dimensions of the PDF pages
and computes the coordinates expected by the server.

```python
from secure_exchanges_sdk.helpers.zone_builder import ZoneBuilder

with ZoneBuilder(pdf_path, is_pki=True) as builder:
    zone = builder.create_field_sign_zone(
        page=1, field_name="signature1",
        x=60, y=400, width=200, height=50
    )
```

Zone types: signature (`create_field_sign_zone`), signature date
(`create_field_date_sign_zone`), initials, text, number, date, checkbox,
radio buttons, text area, option list, dropdown list.

### CryptoHelper

Low-level cryptographic functions (normally used indirectly through the helpers).

| Category | Methods |
|----------|---------|
| **AES-256-CBC** | `encrypt_string_to_bytes`, `decrypt_string_from_bytes`, `encrypt_binary`, `decrypt_binary_from_bytes`, `encrypt_string_to_b64`, `decrypt_string_from_base64`, `encrypt_file`, `decrypt_file` |
| **RSA** | `create_new_private_key`, `get_public_key_from_private_key`, `encrypt_rsa_content`, `decrypt_rsa_content` |
| **Hashing** | `get_sha512_hash_of_bytes`, `get_sha512_hash_of_string`, `get_sha256_hash_of_bytes`, `get_sha512_of_file` |
| **.NET GUIDs** | `guid_to_bytes_le`, `bytes_le_to_guid` |
| **Keys** | `generate_aes_keys`, `generate_secure_random_byte_array` |
| **Utilities** | `alter_binary`, `bytes_xor`, `concat_byte_arrays`, `hex_to_byte_array` |

---

## Data models

### RecipientInfo

```python
RecipientInfo(
    email="user@example.com",
    phone="+15145551234"  # Optional, enables SMS 2FA
)
```

### SignZoneDetails

Defines a signature zone on a PDF. In practice, you do not instantiate
`SignZoneDetails` yourself: zones are created via
[`ZoneBuilder`](#zonebuilder), which computes the coordinates for you.

```python
SignZoneDetails(
    type="sign",         # "sign", "initial", "stamp", "text", "checkbox"
    page=1,              # Page number (1-indexed)
    left=100,            # X position (pixels)
    top=400,             # Y position (pixels)
    width=200,           # Width (pixels)
    height=80,           # Height (pixels)
    page_height=792,     # Page height
    canvas_width=612,    # Canvas width
    canvas_height=792,   # Canvas height
    rotate_degree=0,     # Rotation (0, 90, 180, 270)
    default_value=None   # JSON string with field metadata (required for PKI)
)
```

### RecipientZoneDefinition

Associates signature zones with a recipient for one or more files.

```python
RecipientZoneDefinition(
    email="signer@example.com",
    phone=None,
    zones_def_by_file=[
        FileZoneDefinition(
            unique_name=sha512,          # SHA512 of the file
            do_not_encrypt_pdf=False,
            do_not_append_certificate_to_file=False,
            is_pki_file=True,            # PKI certification
            zones_def=SignZoneDefinition(
                mode="define",
                zones=[...]
            )
        )
    ]
)
```

### MultiRecipientAnswer

Response of `multi_recipient_message`:

| Property | Type | Description |
|----------|------|-------------|
| `status` | `int` | Status code (200=success) |
| `data` | `str` | Descriptive message |
| `recipients_answer` | `List[RecipientAnswer]` | Answers per recipient |

Each `RecipientAnswer` contains a `SendMessageAnswer` with `url` (the link to send to the recipient).

---

## Support

For any questions or issues, contact supportdev@secure-exchanges.info or visit [secure-exchanges.com](https://www.secure-exchanges.com). Product documentation: [help.secure-exchanges.com](https://help.secure-exchanges.com/).

---

## License

Licensed under the Apache License, Version 2.0. Full text at
<https://www.apache.org/licenses/LICENSE-2.0>. The copyright notice is in the
`NOTICE` file.

Using the SDK also requires a Secure Exchanges licence (serial, API user and API
password) obtained from your administration portal. The Apache licence covers
this source code, not access to the service.

---

# Secure Exchanges Python SDK (version française)

[English](#secure-exchanges-python-sdk) | **Français**

SDK Python pour l'API Secure Exchanges : transfert de fichiers sécurisé, messagerie chiffrée de bout en bout et signature numérique de documents PDF.

Le SDK prend en charge l'intégralité du protocole : handshake post-quantique, chiffrement bout-en-bout des messages et des fichiers, signatures électroniques avec certification PKI et journaux de messages.

---

## Table des matières

1. [Installation](#installation-1)
2. [Prérequis](#prérequis)
3. [Identifiants et configuration](#identifiants-et-configuration)
   - [Où trouver vos identifiants](#où-trouver-vos-identifiants)
   - [Configuration](#configuration-1)
   - [Protéger vos identifiants](#protéger-vos-identifiants)
4. [Sécurité](#sécurité)
5. [Architecture du SDK](#architecture-du-sdk)
6. [Guide d'utilisation](#guide-dutilisation)
   - [Envoi d'un message simple](#envoi-dun-message-simple)
   - [Envoi avec mot de passe et code SMS (2FA)](#envoi-avec-mot-de-passe-et-code-sms-2fa)
   - [Envoi avec fichiers joints](#envoi-avec-fichiers-joints)
   - [Envoi avec certification PKI](#envoi-avec-certification-pki)
   - [Récupération d'un message](#récupération-dun-message)
   - [Journaux de messages](#journaux-de-messages)
   - [Suppression d'un message](#suppression-dun-message)
   - [Enveloppes de réponse](#enveloppes-de-réponse)
7. [Référence API](#référence-api)
   - [MessageHelper](#messagehelper-1)
   - [LogsHelper](#logshelper-1)
   - [SignHelper](#signhelper-1)
   - [PkiFileHelper](#pkifilehelper-1)
   - [ZoneBuilder](#zonebuilder-1)
   - [CryptoHelper](#cryptohelper-1)
8. [Modèles de données](#modèles-de-données)
9. [Support](#support-1)
10. [Licence](#licence)

---

## Installation

```bash
pip install secure-exchanges-sdk
```

Cela installe tout le nécessaire pour l'envoi de messages, les pièces jointes,
les journaux et la validation des numéros de téléphone.

### Signature électronique sur PDF (extra `signature`)

La définition de zones de signature (`ZoneBuilder`) et la certification PDF
(`PkiFileHelper`) demandent deux dépendances supplémentaires, `pypdf` et
`playwright`, volontairement non installées par défaut : `playwright`
télécharge un navigateur d'environ 150 Mo :

```bash
pip install "secure-exchanges-sdk[signature]"
playwright install chromium
```

---

## Prérequis

Pour utiliser le SDK, vous devez disposer de :

- **Python 3.10+**
- **Un numéro de série** (`serial`) : UUID de votre licence
- **Un identifiant API** (`api_user`) : UUID de l'utilisateur API
- **Un mot de passe API** (`api_password`) : UUID du mot de passe API

---

## Identifiants et configuration

### Où trouver vos identifiants

Les 3 identifiants nécessaires (`serial`, `api_user`, `api_password`) sont des UUID (GUIDs) que vous obtenez dans votre **portail d'administration Secure Exchanges** lors de la création de votre licence API.

| Identifiant | Description | Exemple |
|-------------|-------------|---------|
| `serial` | Numéro de série de votre licence | `UUID("d17ca972-5f70-4961-bac4-98e6b91b1dc4")` |
| `api_user` | Identifiant de l'utilisateur API | `UUID("0fa0cf77-4ca5-4406-8623-1ed0ffd47290")` |
| `api_password` | Mot de passe de l'utilisateur API | `UUID("4bfeabdf-c498-448a-9255-1e17c3ef8c0b")` |

### Configuration

`SettingsHelper` sélectionne l'environnement et retourne ses endpoints, rien
d'autre. Le SDK ne stocke aucun identifiant et ne va jamais les chercher dans
l'environnement du processus : le serial, l'utilisateur et le mot de passe API
vous appartiennent, et sont passés explicitement à chaque appel.

```python
from secure_exchanges_sdk.client import SecureExchangesClient, SEMSClient
from secure_exchanges_sdk.helpers.settings_helper import SettingsHelper

cfg = SettingsHelper.configure_production()

client      = SecureExchangesClient(cfg['api_endpoint'])
sems_client = SEMSClient(cfg['sems_endpoint'])

# charger_mes_identifiants() est votre propre fonction : elle retourne les trois UUID
# de votre licence (uuid.UUID, pas str). Voir « Protéger vos identifiants »
# ci-dessous pour savoir où les conserver.
serial, api_user, api_password = charger_mes_identifiants()
```

| Fonction | Environnement | Endpoints |
|----------|---------------|-----------|
| `configure_production()` | production | `www.secure-exchanges.com` |
| `configure_preview()` | preview (test) | `preview.secure-exchanges.com` |

Chacune retourne un dictionnaire avec `api_endpoint`, `sems_endpoint`,
`file_handler`, `file_upload_handler` et `environment`.

> Vos identifiants preview et production sont différents : un serial de
> production ne fonctionnera pas en preview, et inversement.

### Protéger vos identifiants

Ces trois UUID valent un mot de passe sur votre compte Secure Exchanges :
quiconque les détient peut envoyer, lire et supprimer des messages en votre
nom, et la consommation vous est facturée. Leur stockage est entièrement votre
décision, et votre responsabilité.

- **Ne les committez jamais.** Gardez-les hors du dépôt, et vérifiez aussi
  l'historique : `git log -S <serial>` retrouve un secret committé une fois puis
  retiré. Un secret parti sur un remote est compromis : il faut le régénérer,
  pas seulement supprimer la ligne.
- **Ne les écrivez jamais en dur dans le code.** Ils se retrouvent dans les
  traces d'exception, dans les rapports de bogue, dans les paquets que vous
  distribuez et dans chaque fork du code.
- **Privilégiez un gestionnaire de secrets.** Chiffrés au repos, accès
  journalisé, rotation possible sans redéploiement.
- **Si vous les gardez dans un fichier, chiffrez-le ou verrouillez-le.** Hors
  de la racine web, appartenant au compte de service, permissions `0400`, et
  exclu de vos sauvegardes ou chiffré à l'intérieur.
- **Méfiez-vous de l'environnement du processus.** Les variables
  d'environnement sont lisibles depuis `/proc/<pid>/environ` par tout ce qui
  tourne sous le même utilisateur, et apparaissent dans les vidages mémoire
  comme dans la plupart des rapporteurs d'erreurs. Chargez les identifiants au
  moment où vous en avez besoin plutôt que de les exporter pour toute la durée
  de vie du processus.
- **Tenez-les hors de vos journaux.** Excluez-les explicitement de votre
  gestionnaire d'erreurs, de votre rapporteur d'incidents et des vidages de requête,
  qui capturent l'environnement et les variables locales par défaut.
- **Régénérez-les depuis le portail d'administration** dès que vous soupçonnez
  une exposition : un portable perdu, un journal transmis à un tiers, le départ
  d'un développeur.

---

## Sécurité

- **Chiffrement de bout en bout** : le contenu (corps du message et fichiers) est
  chiffré localement, avec des clés générées sur votre machine et **jamais
  transmises au serveur** : le serveur ne stocke et ne relaie que des données
  chiffrées.
- **Échange de clés post-quantique** : **ML-KEM-1024** (FIPS 203), résistant aux
  attaques par ordinateur quantique, signé en **ML-DSA-87** (FIPS 204).
- **Chiffrement AES-256** du contenu et des échanges avec le serveur.
- **Protection anti-interception** : toute tentative d'attaque de type homme du
  milieu (MITM) fait échouer la connexion.
- **Rien à gérer** : le SDK applique automatiquement l'intégralité du protocole
  à chaque appel : vous n'avez aucune primitive cryptographique à manipuler.

---

## Architecture du SDK

Survol des grands modules. Dans la pratique, vous utiliserez surtout les
*helpers*, qui orchestrent le protocole complet pour vous :

| Module | Rôle |
|--------|------|
| `client/` | Clients HTTP : `SecureExchangesClient` (API principale), `SEMSClient` (livraison des courriels) |
| `helpers/message_helper.py` | Envoi, lecture, suppression de messages, enveloppes de réponse |
| `helpers/handshake_helper.py` | Handshake post-quantique (ML-KEM-1024 + ML-DSA-87) |
| `helpers/post_quantum_crypto_helper.py` | Primitives post-quantiques du handshake |
| `helpers/crypto_helper.py` | Cryptographie symétrique/RSA de bas niveau (AES-256, SHA-512, GUID .NET) |
| `helpers/logs_helper.py` | Récupération des journaux de messages |
| `helpers/sign_helper.py`, `helpers/zone_builder.py` | Zones de signature électronique sur PDF |
| `helpers/pki_file_helper.py` | Certification PKI de PDF (navigateur headless) |
| `helpers/file_helper.py` | Upload/download chiffré de fichiers (par morceaux, parallèle) |
| `helpers/settings_helper.py` | Configuration (environnements, endpoints) |
| `helpers/contact_helper.py`, `licence_helper.py` | Contacts, validation de licence |
| `helpers/email_helper.py`, `phone_helper.py`, `mime_helper.py` | Validations (courriel, téléphone, types MIME) |
| `models/` | Modèles de données : réponses (`answer/`), entités (`entity/`), énumérations (`enum/`), transport, JSON |
| `callback/` | Modèles pour les callbacks de notification |

---

## Guide d'utilisation

Tous les exemples ci-dessous supposent la configuration suivante (voir
[Configuration](#configuration-1)) :

```python
from secure_exchanges_sdk.client import SecureExchangesClient, SEMSClient
from secure_exchanges_sdk.helpers.settings_helper import SettingsHelper

cfg = SettingsHelper.configure_production()

client      = SecureExchangesClient(cfg['api_endpoint'])
sems_client = SEMSClient(cfg['sems_endpoint'])

# Votre propre fonction : elle retourne les trois UUID de votre licence.
serial, api_user, api_password = charger_mes_identifiants()
```

### Envoi d'un message simple

```python
from secure_exchanges_sdk.helpers.message_helper import MessageHelper
from secure_exchanges_sdk.models.entity.recipient_info import RecipientInfo

recipients = [
    RecipientInfo(email="destinataire@example.com")
]

answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Bonjour, voici un message sécurisé.</p>",
    subject="Message confidentiel",
    culture_id="fr-CA"
)

if answer.status == 200:
    print("Message envoyé avec succès!")
    for ra in answer.recipients_answer:
        if ra.answer and ra.answer.url:
            print(f"  URL: {ra.answer.url}")
else:
    print(f"Erreur {answer.status}: {answer.data}")
```

### Envoi avec mot de passe et code SMS (2FA)

```python
from secure_exchanges_sdk.models.enum.send_method_enum import SendMethodEnum

recipients = [
    RecipientInfo(email="user@example.com", phone="+15145551234")
]

answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Document confidentiel ci-joint.</p>",
    subject="Document protégé",
    password="MotDePasseSecret123",
    send_method=SendMethodEnum.EMAIL_WITH_SMS_CODE,  # lien par courriel, code par SMS
    culture_id="fr-CA",
    maximum_open_time=3,    # Nombre max d'ouvertures (1-99)
    minutes_expiration=1440 # Expiration en minutes (1440 = 24h)
)
```

Valeurs de `SendMethodEnum` :

| Valeur | Description |
|--------|-------------|
| `ONLY_EMAIL` (0) | Lien envoyé par courriel uniquement (défaut) |
| `SMS_ONLY` (1) | Lien envoyé par SMS uniquement |
| `SMS_WITH_EMAIL_CODE` (2) | Lien par SMS, code d'ouverture par courriel |
| `EMAIL_WITH_SMS_CODE` (3) | Lien par courriel, code d'ouverture par SMS |

> La protection par mot de passe (`password`) est indépendante du canal d'envoi
> et peut être combinée avec n'importe quelle valeur de `send_method`.

### Envoi avec fichiers joints

```python
# Depuis le disque
answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Voir fichiers joints.</p>",
    subject="Fichiers sécurisés",
    files_path=["/chemin/vers/document.pdf", "/chemin/vers/image.png"]
)

# Depuis la mémoire (bytes)
pdf_bytes = open("/chemin/vers/rapport.pdf", "rb").read()
csv_bytes = open("/chemin/vers/donnees.csv", "rb").read()

files_in_memory = [
    {"binary": pdf_bytes, "file_name": "rapport.pdf"},
    {"binary": csv_bytes, "file_name": "donnees.csv"}
]

answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=recipients,
    message="<p>Rapport et données en pièces jointes.</p>",
    subject="Rapport mensuel",
    files_list=files_in_memory
)
```

### Envoi avec certification PKI

La certification PKI appose un certificat numérique sur un PDF avant l'envoi pour signature. Le processus utilise un navigateur headless (Playwright, extra `signature`). Les zones de signature se définissent avec `ZoneBuilder`, qui lit les dimensions réelles des pages du PDF :

```python
import asyncio
from secure_exchanges_sdk.helpers.crypto_helper import CryptoHelper
from secure_exchanges_sdk.helpers.zone_builder import ZoneBuilder
from secure_exchanges_sdk.helpers.sign_helper import SignHelper
from secure_exchanges_sdk.helpers.pki_file_helper import PkiFileHelper, CertifyPdfArgs
from secure_exchanges_sdk.models.entity.sign_zone_definition import SignZoneDefinition
from secure_exchanges_sdk.models.transport.file_zone_definition import FileZoneDefinition
from secure_exchanges_sdk.models.transport.recipient_zone_definition import RecipientZoneDefinition

# 1. Lire le PDF et calculer son SHA512
pdf_path = "contrat.pdf"
with open(pdf_path, "rb") as f:
    pdf_bytes = f.read()
sha512 = CryptoHelper.get_sha512_hash_of_bytes(pdf_bytes)

# 2. Définir les zones de signature avec ZoneBuilder
with ZoneBuilder(pdf_path, is_pki=True) as zone_builder:
    zones = [
        # Zone de signature en page 1
        zone_builder.create_field_sign_zone(
            page=1, field_name="signature1",
            x=60, y=400, width=200, height=50
        ),
        # Zone de date remplie automatiquement à la signature
        zone_builder.create_field_date_sign_zone(
            page=1, field_name="date1",
            x=60, y=500, width=100, height=25, font_size=12
        ),
    ]

# 3. Associer les zones au destinataire (fichier référencé par son SHA512)
recipient_zones = [
    RecipientZoneDefinition(
        email="signataire@example.com",
        phone=None,
        zones_def_by_file=[
            FileZoneDefinition(
                unique_name=sha512,
                do_not_encrypt_pdf=False,
                do_not_append_certificate_to_file=False,
                is_pki_file=True,
                zones_def=SignZoneDefinition(mode="define", zones=zones)
            )
        ]
    )
]

# 4. Obtenir le token de certification
cert_response = SignHelper.get_certification_token(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    original_hash=sha512,
    culture_id="fr-CA",
    token_type="CertifyDocument"
)
cert_data = cert_response["certification_data"]

# 5. Certifier le PDF via navigateur headless
certify_args = CertifyPdfArgs(
    file_bytes=pdf_bytes,
    file_name="contrat.pdf",
    recipients=["signataire@example.com"],
    certify_data=cert_data,
    detect_existing_fields=True,
    recipient_zone_definitions=recipient_zones
)

async def certify():
    async with PkiFileHelper(is_preview=False, culture="fr-CA") as helper:
        return await helper.certify_pki_pdf(certify_args)

certified = asyncio.run(certify())

# 6. Envoyer le PDF certifié
answer = MessageHelper.multi_recipient_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    recipients=[RecipientInfo(email="signataire@example.com")],
    message="<p>Veuillez signer ce document certifié.</p>",
    subject="Document PKI à signer",
    certified_pdfs=[certified],
    owner_dont_need_to_sign=True,
    sems_client=sems_client
)
```

Un exemple complet et exécutable, incluant les **options de signature** (`SignOptions` : restriction du mode de signature, demande de fichier en retour) et les **paramètres de callback** (clés contextuelles), se trouve dans `example_pki_with_signature_options.py` à la racine de `pythonSDK/`.

### Récupération d'un message

```python
# Parser un lien Secure Exchanges
link = "https://www.secure-exchanges.com/message.aspx?msgid=xxx&sems=yyy&cpart=zzz"
msg_params = MessageHelper.get_secure_exchanges_message_from_link(link)

# Récupérer et déchiffrer le message
response = MessageHelper.get_message(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    msg_params=msg_params,
    password="MotDePasse"     # si protégé
)
```

> **Note** : chaque lecture consomme une ouverture du message
> (`maximum_open_time`).

### Journaux de messages

Suivez le cycle de vie de vos messages (envoi, ouvertures, signatures) :

```python
from datetime import datetime, timedelta
from uuid import UUID
from secure_exchanges_sdk.helpers.logs_helper import LogsHelper

# Tous les logs des 60 derniers jours
logs_response = LogsHelper.get_logs(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    from_date=datetime.now() - timedelta(days=60),
    to_date=datetime.now()
)

# Le log d'un message précis (par message_id OU tracking_id)
log_response = LogsHelper.get_log(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    tracking_id=UUID("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
)
```

### Suppression d'un message

```python
from uuid import UUID

success = MessageHelper.delete_message_by_tracking_id(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    tracking_id=UUID("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
)
```

### Enveloppes de réponse

Les enveloppes permettent à un destinataire de vous répondre de manière sécurisée :

```python
# Enveloppe simple (un destinataire)
env_response = MessageHelper.get_enveloppe(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    subject="Envoyez-moi vos documents",
    destination="destinataire@example.com",
    culture_id="fr-CA",
    reply_expiration_hours=48,
    maximum_reply_open_time=3,
    reply_to_api=True,
    authorized_extensions=[".pdf", ".docx"]
)

if env_response.url:
    print(f"URL de l'enveloppe: {env_response.url}")

# Enveloppes multiples
env_responses = MessageHelper.get_envelopes(
    api_client=client,
    serial=serial,
    api_user=api_user,
    api_password=api_password,
    subject="Documents requis",
    recipients=[
        {"Email": "user1@example.com"},
        {"Email": "user2@example.com"}
    ]
)
```

---

## Référence API

### MessageHelper

Classe statique pour l'envoi et la réception de messages.

| Méthode | Description |
|---------|-------------|
| `multi_recipient_message(api_client, serial, api_user, api_password, recipients, message, subject, ...)` | Envoie un message à plusieurs destinataires |
| `get_message(api_client, serial, api_user, api_password, msg_params, password=None, digit_code=None, sems_client=None)` | Récupère et déchiffre un message |
| `delete_message_by_tracking_id(api_client, serial, api_user, api_password, tracking_id, ...)` | Supprime un message par tracking ID |
| `get_enveloppe(api_client, serial, api_user, api_password, subject, destination, ...)` | Crée une enveloppe de réponse |
| `get_envelopes(api_client, serial, api_user, api_password, subject, recipients, ...)` | Crée des enveloppes multiples |
| `get_secure_exchanges_message_from_link(link)` | Parse un lien SE en paramètres |
| `send_trace(api_client, trace_content)` | Envoie une trace de support |

#### Paramètres de `multi_recipient_message`

| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `api_client` | `SecureExchangesClient` | requis | Client API |
| `serial` | `UUID` | requis | Numéro de série |
| `api_user` | `UUID` | requis | Identifiant API |
| `api_password` | `UUID` | requis | Mot de passe API |
| `recipients` | `List[RecipientInfo]` | requis | Destinataires |
| `message` | `str` | requis | Contenu HTML du message |
| `subject` | `str` | requis | Sujet du message |
| `password` | `str` | `None` | Mot de passe de protection |
| `files_list` | `List[dict]` | `None` | Fichiers en mémoire (`{"binary": bytes, "file_name": str}`) |
| `files_path` | `List[str]` | `None` | Chemins de fichiers sur le disque |
| `send_method` | `SendMethodEnum` | `ONLY_EMAIL` | Canal d'envoi (voir tableau `SendMethodEnum` plus haut) |
| `get_back_html` | `bool` | `True` | `True`=vous envoyez l'email, `False`=SEMS envoie |
| `show_subject` | `bool` | `True` | Afficher le sujet dans la notification |
| `get_notify` | `bool` | `True` | Notifier à l'ouverture |
| `culture_id` | `str` | `"en-US"` | Langue (`"fr-CA"`, `"en-US"`) |
| `maximum_open_time` | `int` | `1` | Nombre max d'ouvertures (1-99) |
| `minutes_expiration` | `int` | `10080` | Expiration en minutes (défaut : 7 jours) |
| `create_message_copy` | `bool` | `False` | Copie pour l'expéditeur |
| `name` | `str` | `"Secure Exchanges"` | Nom de l'expéditeur |
| `callback_parameters` | `str` | `None` | Paramètres libres (JSON) renvoyés dans les callbacks de notification |
| `reply_options` | `dict` | `None` | Options de réponse du destinataire (voir ci-dessous) |
| `owner_dont_need_to_sign` | `bool` | `True` | L'expéditeur ne signe pas |
| `certified_pdfs` | `List[CertifiedPdfContainer]` | `None` | PDFs certifiés PKI |
| `sign_options` | `SignOptions` | `None` | Options de signature : restriction de mode, destinataires CC/BCC, options d'upload |
| `sems_client` | `SEMSClient` | `None` | Client SEMS (créé automatiquement si omis) |

`reply_options` est un dictionnaire (clés PascalCase, comme l'API) qui contrôle
ce que le destinataire peut faire en réponse :

```python
from secure_exchanges_sdk.models.enum.reply_file_upload_mode import ReplyFileUploadMode

reply_options = {
    "UploadMode": int(ReplyFileUploadMode.UPLOAD_MANDATORY),  # NO_UPLOAD, UPLOAD_OPTIONAL, UPLOAD_MANDATORY
    "AuthorizedExtensions": [".jpg", ".png", ".pdf"],
    "Messages": [
        {"CultureID": "fr-CA", "Message": "Veuillez joindre votre document."},
        {"CultureID": "en-CA", "Message": "Please attach your document."},
    ],
    "NoEditor": False,   # désactiver l'éditeur de texte de réponse
    "NoReply": False,    # interdire toute réponse
}
```

`sign_options` (`SignOptions`, dans `models/json_models/sign_options.py`)
permet, pour un envoi en signature : `signature_restriction`
(`SignatureModeRestriction`), `cc_recipients` / `bcc_recipients`
(destinataires en copie, non-signataires) et `upload_file_options`.

### LogsHelper

Récupération des journaux de messages (envois, ouvertures, activités).

| Méthode | Description |
|---------|-------------|
| `get_logs(api_client, serial, api_user, api_password, from_date, to_date, user_log_serial=None)` | Tous les logs sur une plage de dates (`datetime`) → `GetLogsResponse` |
| `get_log(api_client, serial, api_user, api_password, message_id=None, tracking_id=None, user_log_serial=None)` | Log d'un message précis : fournir `message_id` **ou** `tracking_id` → `GetLogResponse` |

### SignHelper

Gestion des zones de signature numérique sur les PDF.

| Méthode | Description |
|---------|-------------|
| `add_file_zone_def_to_recipient_index(recipient_index, template_zones, sha512, recipients_match)` | Convertit des templates en définitions de zones |
| `convert_recipient_index_to_list(recipient_index)` | Convertit l'index en liste pour `multi_recipient_message` |
| `concat_recipient_zone_definition_lists(list1, list2)` | Fusionne deux listes de définitions de zones |
| `validate_all_recipients_have_zones(files_to_sign, recipients, recipients_zone_def)` | Vérifie que tous les destinataires ont des zones |
| `validate_document_signature_integrity(api_client, file_sha512)` | Valide l'intégrité d'un document signé |
| `get_certification_token(api_client, serial, api_user, api_password, original_hash, culture_id, ...)` | Obtient un token de certification PKI |

### PkiFileHelper

Certification PKI de PDF via navigateur headless (Playwright, extra `signature`).

```python
# Asynchrone (recommandé)
async with PkiFileHelper(is_preview=True, culture="fr-CA") as helper:
    result = await helper.certify_pki_pdf(args)

# Synchrone
helper = PkiFileHelper(is_preview=True)
result = helper.certify_pki_pdf_sync(args)
helper.close_sync()
```

#### CertifyPdfArgs

| Propriété | Type | Description |
|-----------|------|-------------|
| `file_bytes` | `bytes` | Contenu binaire du PDF |
| `file_name` | `str` | Nom du fichier |
| `recipients` | `List[str]` | Emails des destinataires |
| `certify_data` | `dict` | Token de certification (de `get_certification_token`) |
| `detect_existing_fields` | `bool` | Détecter les champs existants (défaut: `True`) |
| `recipient_zone_definitions` | `List[RecipientZoneDefinition]` | Zones prédéfinies |
| `define_recipients` | `Callable` | Callback pour distribuer les zones entre destinataires |

#### CertifiedPdfContainer

| Propriété | Type | Description |
|-----------|------|-------------|
| `file_bytes` | `bytes` | PDF certifié |
| `file_name` | `str` | Nom du fichier |
| `sign_file_def` | `dict` | Définition de signature (`SHA512`, `IsPKIFile`, etc.) |
| `recipient_zone_definitions` | `List[RecipientZoneDefinition]` | Zones mises à jour avec le nouveau SHA512 |

### ZoneBuilder

Construction de zones de signature à partir d'un PDF (`helpers/zone_builder.py`,
extra `signature`). Le builder lit les dimensions réelles des pages du PDF et
calcule les coordonnées attendues par le serveur.

```python
from secure_exchanges_sdk.helpers.zone_builder import ZoneBuilder

with ZoneBuilder(pdf_path, is_pki=True) as builder:
    zone = builder.create_field_sign_zone(
        page=1, field_name="signature1",
        x=60, y=400, width=200, height=50
    )
```

Types de zones : signature (`create_field_sign_zone`), date de signature
(`create_field_date_sign_zone`), initiales, texte, nombre, date, case à cocher,
boutons radio, zone de texte, liste d'options, liste déroulante.

### CryptoHelper

Fonctions cryptographiques de bas niveau (normalement utilisées indirectement via les helpers).

| Catégorie | Méthodes |
|-----------|----------|
| **AES-256-CBC** | `encrypt_string_to_bytes`, `decrypt_string_from_bytes`, `encrypt_binary`, `decrypt_binary_from_bytes`, `encrypt_string_to_b64`, `decrypt_string_from_base64`, `encrypt_file`, `decrypt_file` |
| **RSA** | `create_new_private_key`, `get_public_key_from_private_key`, `encrypt_rsa_content`, `decrypt_rsa_content` |
| **Hachage** | `get_sha512_hash_of_bytes`, `get_sha512_hash_of_string`, `get_sha256_hash_of_bytes`, `get_sha512_of_file` |
| **GUID .NET** | `guid_to_bytes_le`, `bytes_le_to_guid` |
| **Clés** | `generate_aes_keys`, `generate_secure_random_byte_array` |
| **Utilitaires** | `alter_binary`, `bytes_xor`, `concat_byte_arrays`, `hex_to_byte_array` |

---

## Modèles de données

### RecipientInfo

```python
RecipientInfo(
    email="user@example.com",
    phone="+15145551234"  # Optionnel, active le 2FA par SMS
)
```

### SignZoneDetails

Définit une zone de signature sur un PDF. En pratique, vous n'instanciez pas
`SignZoneDetails` vous-même : les zones sont créées via
[`ZoneBuilder`](#zonebuilder-1), qui calcule les coordonnées pour vous.

```python
SignZoneDetails(
    type="sign",         # "sign", "initial", "stamp", "text", "checkbox"
    page=1,              # Numéro de page (1-indexé)
    left=100,            # Position X (pixels)
    top=400,             # Position Y (pixels)
    width=200,           # Largeur (pixels)
    height=80,           # Hauteur (pixels)
    page_height=792,     # Hauteur de la page
    canvas_width=612,    # Largeur du canvas
    canvas_height=792,   # Hauteur du canvas
    rotate_degree=0,     # Rotation (0, 90, 180, 270)
    default_value=None   # JSON string avec métadonnées du champ (requis pour PKI)
)
```

### RecipientZoneDefinition

Associe des zones de signature à un destinataire pour un ou plusieurs fichiers.

```python
RecipientZoneDefinition(
    email="signataire@example.com",
    phone=None,
    zones_def_by_file=[
        FileZoneDefinition(
            unique_name=sha512,          # SHA512 du fichier
            do_not_encrypt_pdf=False,
            do_not_append_certificate_to_file=False,
            is_pki_file=True,            # certification PKI
            zones_def=SignZoneDefinition(
                mode="define",
                zones=[...]
            )
        )
    ]
)
```

### MultiRecipientAnswer

Réponse de `multi_recipient_message` :

| Propriété | Type | Description |
|-----------|------|-------------|
| `status` | `int` | Code de statut (200=succès) |
| `data` | `str` | Message descriptif |
| `recipients_answer` | `List[RecipientAnswer]` | Réponses par destinataire |

Chaque `RecipientAnswer` contient un `SendMessageAnswer` avec `url` (le lien à envoyer au destinataire).

---

## Support

Pour toute question ou problème, contactez supportdev@secure-exchanges.info ou visitez [secure-exchanges.com](https://www.secure-exchanges.com). Documentation produit : [help.secure-exchanges.com](https://help.secure-exchanges.com/).

---

## Licence

Distribué sous licence Apache, version 2.0. Texte complet sur
<https://www.apache.org/licenses/LICENSE-2.0>. Le copyright figure dans le
fichier `NOTICE`.

L'utilisation du SDK requiert par ailleurs une licence Secure Exchanges (serial,
usager et mot de passe API) obtenue depuis votre portail d'administration. La
licence Apache couvre ce code source, pas l'accès au service.
