# Documentation de l'API Signoui

Signature électronique hébergée en France, éditée par HSLP Labs.
Version web : https://signoui.fr/documentation/ · Référence OpenAPI : https://api.signoui.fr/v1/docs · Schéma : https://api.signoui.fr/v1/openapi.json

## Démarrage rapide

Vous voulez faire signer un PDF depuis votre logiciel, votre CRM ou votre site ? Il faut cinq appels : créer un dossier, y ajouter le PDF, ajouter le signataire, placer son champ de signature, puis envoyer. Signoui s'occupe du reste : e-mail d'invitation, code de vérification, scellement du PDF, dossier de preuve, et un webhook pour vous prévenir.

1. **Créez un compte** sur [app.signoui.fr](https://app.signoui.fr/app/inscription) (2 envois offerts par mois, sans carte bancaire).
2. **Créez une clé de test** dans l'espace client, rubrique **Développeurs**. Une clé `sk_test_` n'envoie aucun e-mail ni SMS et ne consomme aucun envoi.
3. **Lancez le parcours complet** ci-dessous, puis ouvrez le lien `test_sign_urls` renvoyé par l'envoi pour signer vous-même dans votre navigateur.
4. **Passez en production** avec une clé `sk_live_` distincte : rien d'autre ne change dans votre code.

```bash
KEY="sk_test_xxx"
API="https://api.signoui.fr/v1"

# 1. Créer le dossier (brouillon)
ENV=$(curl -s -X POST "$API/envelopes" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: devis-2026-0412" \
  -d '{"title":"Devis n° 2026-0412","workflow":"sequential","message":"Bonjour, voici le devis dont nous avons parlé.","expires_in_days":30,"metadata":{"crm_id":"12345"}}' \
  | jq -r .id)

# 2. Ajouter le PDF (20 Mo et 300 pages maximum)
DOC=$(curl -s -X POST "$API/envelopes/$ENV/documents" \
  -H "Authorization: Bearer $KEY" \
  -F "file=@devis-2026-0412.pdf;type=application/pdf" | jq -r .id)

# 3. Ajouter un signataire (téléphone au format international pour le code SMS)
SGN=$(curl -s -X POST "$API/envelopes/$ENV/signers" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"full_name":"Marie Dupont","email":"marie.dupont@exemple.fr","phone":"+33612345678","otp_channel":"sms"}' \
  | jq -r .id)

# 4. Placer son champ de signature (points PDF, origine en bas à gauche de la page)
curl -s -X POST "$API/envelopes/$ENV/fields" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"document_id\":\"$DOC\",\"signer_id\":\"$SGN\",\"kind\":\"signature\",\"page\":3,\"x\":60,\"y\":80,\"width\":180,\"height\":60}"

# 5. Envoyer à la signature
curl -s -X POST "$API/envelopes/$ENV/send" \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: send-devis-2026-0412"
```

Ensuite, attendez le webhook `envelope.completed` (voir [Webhooks](https://signoui.fr/documentation/#webhooks)) ou interrogez `GET /envelopes/{id}`, puis téléchargez le PDF scellé et l'archive de preuve (voir [Télécharger le PDF signé et la preuve](https://signoui.fr/documentation/#télécharger-le-pdf-signé-et-la-preuve)).

## L'essentiel

| Élément | Valeur |
|---|---|
| URL de base | `https://api.signoui.fr/v1` |
| Authentification | en-tête `Authorization: Bearer sk_live_…` ou `sk_test_…` |
| Format | JSON (UTF-8) en entrée et en sortie ; `multipart/form-data` pour l'envoi du PDF |
| Identifiants | préfixés : `env_…` (dossier), `doc_…` (document), `sgn_…` (signataire), `fld_…` (champ), `evt_…` (événement) |
| Dates | ISO 8601 avec fuseau, par exemple `2026-10-07T14:03:11+00:00` |
| Limite de débit | 60 requêtes par minute et par clé (ajustable sur demande), HTTP 429 au-delà |
| Hébergement | 100 % en France (OVH), aucune donnée hors de l'Union européenne |
| Référence OpenAPI | [api.signoui.fr/v1/docs](https://api.signoui.fr/v1/docs) (interactive) et [openapi.json](https://api.signoui.fr/v1/openapi.json) (schéma brut, pour générer un client) |

### Clés de test et clés réelles

Une clé `sk_test_` crée des dossiers en mode `test` : aucun e-mail ni SMS ne part, rien n'est décompté, les liens de signature sont renvoyés dans la réponse pour automatiser vos tests de bout en bout, et les webhooks ne sont pas livrés (ils sont journalisés côté Signoui). Les dossiers de test et réels sont séparés : chaque clé ne voit que ceux de son mode.

Une clé `sk_live_` fait tout pour de vrai : chaque signataire d'un dossier envoyé consomme **un envoi** du solde de votre organisation.

### Portées d'une clé

Chaque clé a ses portées : `envelopes:read`, `envelopes:write`, `documents:download`. Une clé sans la portée nécessaire reçoit un `403 insufficient_scope`. Les clés se créent et se révoquent dans l'espace client, rubrique **Développeurs**.

**La clé API ne doit jamais se trouver dans le navigateur** ni dans une application mobile : appelez l'API depuis votre serveur.

## Le modèle en une minute

Un **dossier** (`envelope`) contient un **document** PDF, un ou plusieurs **signataires**, et des **champs** placés sur le document (au minimum un champ `signature` par signataire).

On crée le dossier en brouillon, on ajoute le PDF, les signataires et les champs, puis on l'**envoie**. Il est alors figé. Chaque signataire reçoit un lien unique, lit le document en entier, remplit ses champs, donne son consentement, puis saisit un code reçu par SMS ou par e-mail, qui vaut signature. Quand tout le monde a signé, Signoui scelle le PDF (signature PAdES, horodatage, page « Certificat de signature »), produit une archive de preuve et vous prévient par webhook.

### Statuts d'un dossier

`draft` → `pending` → `sealing` → `completed`, ou bien :

- `declined` : un signataire a refusé ;
- `expired` : le délai de signature est dépassé ;
- `cancelled` : vous avez annulé le dossier ;
- `failed` : incident de scellement, pris en charge par nos équipes.

### Statuts d'un signataire

`waiting` (pas encore son tour, en circuit séquentiel) → `notified` → `opened` → `consented` → `signed`, ou `declined`.

### Circuits

- `sequential` : les signataires reçoivent l'invitation l'un après l'autre, dans l'ordre de `rank`.
- `parallel` : tous la reçoivent en même temps.

## Points d'entrée

### GET /me

Retourne l'organisation, la clé utilisée et le solde d'envois. Pratique pour vérifier une clé et afficher le solde dans votre outil.

```json
{
  "organization_id": "org_01J8Q…",
  "organization_name": "Martin Rénovation",
  "billing_mode": "prepaid",
  "sends_available": 47,
  "api_key_id": "key_01J8Q…",
  "api_key_name": "Site web",
  "mode": "live",
  "scopes": ["envelopes:read", "envelopes:write", "documents:download"]
}
```

`sends_available` : envois restants (envois offerts du mois et packs). `null` signifie illimité (compte API à facturation mensuelle). Un envoi correspond à un signataire d'un dossier envoyé, code SMS compris.

### GET /me/usage

Votre consommation du mois en cours (en temps universel) et, pour un compte à [facturation mensuelle](https://signoui.fr/documentation/#facturation-mensuelle-comptes-api), le montant de la prochaine facture si le mois s'arrêtait maintenant. Accessible avec n'importe quelle clé. Ajoutez `?period=AAAA-MM` pour un mois passé.

```json
{
  "period": "2026-10",
  "billing_mode": "api_monthly",
  "sends": 212,
  "free_quota": 2,
  "free_used": 2,
  "billable": 210,
  "billable_verified": 150,
  "billable_light": 60,
  "unit_verified_cents": 60,
  "unit_light_cents": 40,
  "minimum_cents": 15000,
  "amount_cents": 15000,
  "sends_available": null,
  "packs_remaining": null
}
```

| Champ | Description |
|---|---|
| `sends` | Envois du mois, offerts compris |
| `free_quota`, `free_used` | Envois offerts du mois, et combien ont été utilisés |
| `billable` | Envois facturables : `billable_verified` (confirmés par un code SMS ou e-mail) + `billable_light` (sans code) |
| `unit_verified_cents`, `unit_light_cents` | Prix unitaires HT en centimes, avec et sans code |
| `minimum_cents` | Minimum mensuel HT en centimes |
| `amount_cents` | Montant HT estimé de la facture : `billable_verified × unit_verified + billable_light × unit_light`, porté au minimum mensuel dès qu'il y a au moins un envoi facturable, `0` sinon. `null` pour un compte prépayé |
| `sends_available`, `packs_remaining` | Pour un compte prépayé : ce qu'il reste à consommer |

Dans l'exemple : 150 × 0,60 € + 60 × 0,40 € = 114 € HT, porté au minimum de 150 € HT. Les montants sont hors taxes ; la TVA de 20 % s'ajoute sur la facture. Les envois faits avec une clé de test ne comptent jamais.

### POST /envelopes

Crée un dossier en brouillon.

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `title` | texte, 300 caractères max. | oui | Titre affiché aux signataires (objet des e-mails) |
| `workflow` | `sequential` ou `parallel` | non (défaut `sequential`) | Ordre de signature |
| `message` | texte, 2 000 caractères max. | non | Message personnel dans l'e-mail d'invitation |
| `expires_in_days` | entier de 1 à 180 | non (défaut 30) | Délai de signature après l'envoi |
| `metadata` | objet JSON, 4 Ko max. | non | Vos propres références (identifiant CRM, numéro de devis…), renvoyées telles quelles |
| `reminders_enabled` | booléen | non (défaut `true`) | Rappels automatiques par e-mail à J+3 et J+7 |

Réponse `201` : l'objet [dossier](https://signoui.fr/documentation/#objet-dossier). Accepte l'en-tête [`Idempotency-Key`](https://signoui.fr/documentation/#idempotence).

### POST /envelopes/{id}/documents

Ajoute le PDF au dossier. Envoi en `multipart/form-data`, champ `file`. Un seul PDF par dossier, 20 Mo et 300 pages maximum, dossier en `draft` uniquement.

```json
{
  "id": "doc_01J8Q…",
  "kind": "original",
  "filename": "devis-2026-0412.pdf",
  "sha256": "8090ce…",
  "size_bytes": 184233,
  "page_count": 3,
  "created_at": "2026-10-07T14:01:52+00:00"
}
```

Conservez `sha256` : c'est l'empreinte du document qui sera scellée et inscrite au journal de preuve.

### POST /envelopes/{id}/signers

Ajoute un signataire.

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `full_name` | texte, 200 caractères max. | oui | Nom complet |
| `email` | e-mail | oui | Unique dans le dossier |
| `phone` | texte | si `otp_channel` vaut `sms` ou `choice`, ou si `delivery` vaut `sms` | Format international, par exemple `+33612345678` |
| `rank` | entier, 1 ou plus | non | Ordre en circuit séquentiel (défaut : à la suite). En parallèle, toujours 1 |
| `otp_channel` | `sms`, `email`, `choice` ou `none` | non (défaut : réglage de l'organisation) | Canal du code qui vaut signature, voir ci-dessous |
| `delivery` | `email`, `sms` ou `none` | non (défaut `email`) | Comment le signataire reçoit son invitation, voir ci-dessous |
| `external_id` | texte, 100 caractères max. | si `otp_channel` vaut `none` | Identifiant du signataire dans votre système, inscrit au dossier de preuve |
| `return_url` | URL https | non | Page de votre site où renvoyer le signataire après signature |
| `flow` | `standard` ou `express` | non (défaut `standard`) | Parcours du signataire, voir ci-dessous |

**Canal du code (`otp_channel`)**

- `sms` (recommandé) : le code arrive par SMS, un canal indépendant de l'e-mail, ce qui renforce l'identification du signataire en cas de contestation.
- `email` : le code arrive par e-mail.
- `choice` : le signataire choisit SMS ou e-mail au moment de signer ; son choix est inscrit au journal de preuve. Téléphone requis.
- `none` : aucun code, réservé à la [signature intégrée](https://signoui.fr/documentation/#signature-intégrée) avec un signataire que vous avez déjà identifié.

**Invitation (`delivery`)**

- `email` : lien de signature par e-mail, avec rappels à J+3 et J+7.
- `sms` : lien de signature par SMS.
- `none` : rien n'est envoyé ; vous ouvrez vous-même le lien dans votre site (voir [Signature intégrée](https://signoui.fr/documentation/#signature-intégrée)).

**Parcours (`flow`)**

- `standard` : lecture du document page par page, puis champs, consentement et code.
- `express` : une seule page avec le document, les champs, le consentement et le bouton de signature. Pour un signataire qui a déjà le document sous les yeux dans votre application (voir [Signature intégrée](https://signoui.fr/documentation/#signature-intégrée)).

Réponse `201` : l'objet signataire (voir [Objet dossier](https://signoui.fr/documentation/#objet-dossier)).

### POST /envelopes/{id}/fields

Place un champ sur le document.

| Champ | Type | Description |
|---|---|---|
| `document_id`, `signer_id` | texte | Document et signataire concernés |
| `kind` | `signature`, `initials`, `text`, `checkbox`, `date` ou `mention` | `mention` : texte à recopier, par exemple « Lu et approuvé » ; `date` est pré-remplie à la date de signature |
| `page` | entier, 1 ou plus | Numéro de page |
| `x`, `y`, `width`, `height` | nombres | **En points PDF (1/72 de pouce), origine en bas à gauche de la page.** Une page A4 mesure 595 × 842 points |
| `required` | booléen (défaut `true`) | Champ obligatoire |
| `label` | texte, 200 caractères max. | Libellé affiché au signataire |

Exemple : une signature en bas à gauche de la page 3 se place avec `"x": 60, "y": 80, "width": 180, "height": 60`.

Chaque signataire doit avoir au moins un champ `signature`, sinon l'envoi est refusé (`signature_field_missing`). Un signataire ne peut remplir que ses propres champs.

### POST /envelopes/{id}/send

Envoie le dossier à la signature. Le dossier est figé, l'empreinte du PDF est inscrite au journal, **un envoi est décompté par signataire**, puis le premier signataire (circuit séquentiel) ou tous les signataires (circuit parallèle) sont invités.

L'appel est idempotent : renvoyer un dossier déjà envoyé retourne le dossier tel quel. Accepte aussi l'en-tête [`Idempotency-Key`](https://signoui.fr/documentation/#idempotence).

Erreurs possibles :

- `402 credits_insufficient` : solde insuffisant, le message indique combien d'envois manquent ;
- `422 document_missing`, `signer_missing` ou `signature_field_missing` : le dossier est incomplet ;
- `409 invalid_transition` : le dossier n'est plus en brouillon.

En mode test, la réponse contient `test_sign_urls`, par exemple `{"sgn_…": "https://app.signoui.fr/sign/…"}`, pour ouvrir le parcours de signature dans un navigateur.

### GET /envelopes et GET /envelopes/{id}

`GET /envelopes?status=pending&limit=25&cursor=…` liste vos dossiers du plus récent au plus ancien, filtrables par `status`. La pagination se fait par curseur : repassez `next_cursor` tel quel ; `null` signifie que vous êtes à la fin.

`GET /envelopes/{id}` retourne l'état complet d'un dossier : l'objet [dossier](https://signoui.fr/documentation/#objet-dossier).

### Télécharger le PDF signé et la preuve

`GET /envelopes/{id}/documents/{document_id}/download`, avec la portée `documents:download`. Un dossier contient jusqu'à trois documents, repérés par `kind` dans `documents[]` :

| `kind` | Disponible | Contenu |
|---|---|---|
| `original` | dès l'ajout | Le PDF tel que vous l'avez fourni |
| `sealed` | statut `completed` | Le PDF signé : champs incrustés, page « Certificat de signature » ajoutée, signature électronique PAdES visible dans Adobe Reader, horodatage. `Content-Type: application/pdf` |
| `evidence` | `completed`, `declined` ou `expired`, quelques secondes après | Archive ZIP de preuve autoportante : PDF original et scellé, journal d'audit chaîné (JSON), certificats, jeton d'horodatage, ancrage OpenTimestamps, notice de vérification. `Content-Type: application/zip` |

Le PDF scellé est aussi vérifiable publiquement, sans compte, sur [app.signoui.fr/verify](https://app.signoui.fr/verify).

### POST /envelopes/{id}/cancel

Annule un dossier en `draft` ou `pending`. Corps facultatif : `{"reason": "…"}`. Les liens de signature deviennent inutilisables et les signataires qui n'ont pas signé sont prévenus. **Les envois décomptés ne sont pas restitués.**

### GET /envelopes/{id}/events

Retourne le journal de preuve du dossier, dans l'ordre chronologique. Chaque événement est chaîné au précédent par une empreinte SHA-256 (`prev_hash` → `hash`) : on ne peut ni retirer ni modifier un événement sans casser la chaîne.

```json
{
  "id": "evt_…",
  "seq": 12,
  "type": "signer.signed",
  "occurred_at": "2026-10-07T14:20:58+00:00",
  "actor": "signer:sgn_…",
  "signer_id": "sgn_…",
  "ip": "92.184.x.x",
  "user_agent": "Mozilla/5.0 (iPhone…)",
  "data": { "document_sha256": "8090ce…", "identification": "email_link+otp_sms" },
  "prev_hash": "…",
  "hash": "…"
}
```

Principaux types : `envelope.created`, `document.added`, `signer.added`, `field.added`, `envelope.sent`, `signer.notified`, `signer.embed_link_created`, `signer.opened`, `document.viewed` (pages et temps de lecture), `field.filled`, `signer.consented` (texte et version du consentement), `otp.sent`, `otp.failed`, `otp.verified`, `signer.signed`, `envelope.all_signed`, `envelope.sealed`, `timestamp.anchored`, `envelope.declined`, `envelope.expired`, `envelope.cancelled`, `reminder.sent`, `operator.unmasked` (consultation de l'identité d'un signataire par notre support, motif inclus).

## Objet dossier

```json
{
  "id": "env_01J8Q…",
  "mode": "live",
  "title": "Devis n° 2026-0412",
  "status": "pending",
  "workflow": "sequential",
  "message": "Bonjour, voici le devis dont nous avons parlé.",
  "metadata": { "crm_id": "12345" },
  "expires_at": "2026-11-06T14:03:11+00:00",
  "sent_at": "2026-10-07T14:03:11+00:00",
  "completed_at": null,
  "closed_at": null,
  "close_reason": null,
  "created_at": "2026-10-07T14:01:40+00:00",
  "documents": [
    { "id": "doc_…", "kind": "original", "filename": "devis.pdf", "sha256": "…",
      "size_bytes": 184233, "page_count": 3, "created_at": "…" }
  ],
  "signers": [
    { "id": "sgn_…", "rank": 1, "full_name": "Marie Dupont", "email": "marie.dupont@exemple.fr",
      "phone": "+33612345678", "otp_channel": "sms", "status": "notified",
      "notified_at": "…", "opened_at": null, "signed_at": null,
      "declined_at": null, "decline_reason": null }
  ],
  "fields": [
    { "id": "fld_…", "document_id": "doc_…", "signer_id": "sgn_…", "kind": "signature",
      "page": 3, "x": 60, "y": 80, "width": 180, "height": 60,
      "required": true, "label": null, "value": null, "filled_at": null }
  ],
  "test_sign_urls": null
}
```

`closed_at` et `close_reason` sont renseignés pour les statuts `declined`, `expired` et `cancelled`. Après signature, `fields[].value` contient la valeur saisie (pour une signature : le nom tapé ou « tracé manuscrit »).

## Webhooks

Signoui prévient votre serveur en temps réel par une requête POST en HTTPS. Les webhooks se gèrent dans l'espace client, rubrique **Développeurs** : URL, événements souscrits, et bouton « Tester » qui envoie un événement `ping`. Le **secret de signature** n'est affiché qu'une fois, à la création : conservez-le côté serveur.

### Événements

| Type | Quand |
|---|---|
| `envelope.sent` | Le dossier est envoyé, le ou les premiers signataires sont invités |
| `signer.signed` | Un signataire a signé (le dossier passe au suivant ou au scellement) |
| `envelope.completed` | Tout le monde a signé : PDF scellé et archive de preuve disponibles |
| `envelope.declined` | Un signataire a refusé (motif dans `signers[].decline_reason`) |
| `envelope.expired` | Délai dépassé sans signature complète |
| `envelope.cancelled` | Dossier annulé (par l'API ou depuis l'espace client) |
| `ping` | Test manuel depuis l'espace client |

Souscrivez à `*` pour tout recevoir, à une liste précise, ou à un motif comme `envelope.*`.

### Corps de la requête

```json
{
  "id": "job_01J8Q…",
  "type": "envelope.completed",
  "created_at": "2026-10-07T14:21:40+00:00",
  "mode": "live",
  "data": {
    "envelope": {
      "id": "env_01J8Q…",
      "status": "completed",
      "title": "Devis n° 2026-0412",
      "signers": [{ "id": "sgn_…", "status": "signed", "rank": 1 }]
    }
  }
}
```

Le webhook est volontairement léger : il ne contient ni donnée personnelle ni document. À réception, appelez `GET /envelopes/{id}` pour l'état complet et, si le statut est `completed`, téléchargez les documents `sealed` et `evidence`.

### Vérifier la signature (obligatoire)

| En-tête | Contenu |
|---|---|
| `Signoui-Event` | Type de l'événement |
| `Signoui-Delivery` | Identifiant unique de la livraison, pour dédoublonner |
| `Signoui-Signature` | `t=<horodatage unix>,v1=<HMAC-SHA256 en hexadécimal>` |
| `User-Agent` | `Signoui-Webhooks/1.0` |

La signature vaut `HMAC-SHA256(secret, "<t>." + corps brut)`. Vérifiez-la **sur le corps brut, avant de le décoder**, et refusez les livraisons dont `t` date de plus de 5 minutes, pour bloquer les rejeux.

Python (Flask, FastAPI, Django) :

```python
import hmac, hashlib, time

def verify_signoui(secret: str, signature_header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t, v1 = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)
```

PHP :

```php
<?php
function verify_signoui(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {
    parse_str(str_replace(',', '&', $header), $p); // ['t' => '…', 'v1' => '…']
    if (abs(time() - (int) $p['t']) > $tolerance) return false;
    $expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $p['v1']);
}
// $raw = file_get_contents('php://input');
// verify_signoui($secret, $_SERVER['HTTP_SIGNOUI_SIGNATURE'], $raw);
```

Node.js (avec Express, utilisez `express.raw({ type: 'application/json' })` pour garder le corps brut) :

```js
const crypto = require("crypto");

function verifySignoui(secret, header, rawBody, tolerance = 300) {
  const p = Object.fromEntries(header.split(",").map((s) => s.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${p.t}.`).update(rawBody).digest("hex");
  return expected.length === p.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}
```

### Livraison et nouvelles tentatives

Répondez par un code `2xx` en moins de 10 secondes ; si votre traitement est long, faites-le en arrière-plan. Toute autre réponse, ou un délai dépassé, déclenche de nouvelles tentatives espacées : 30 secondes, 2 minutes, 8 minutes, 32 minutes, puis 2 heures.

Après le cinquième échec, le webhook est **désactivé** et un événement `webhook.disabled` est enregistré ; réactivez-le depuis l'espace client une fois le problème corrigé. Une même livraison peut exceptionnellement arriver deux fois : dédoublonnez sur `Signoui-Delivery`.

L'URL doit être en HTTPS et publique (pas d'adresse de réseau privé), et les redirections ne sont pas suivies.

## Signature intégrée

Votre client est déjà connecté sur votre site ou dans votre application ? Il peut signer sans en sortir. Trois réglages indépendants, passés à la création du signataire, à combiner selon votre cas :

| Réglage | Valeurs | Ce que ça change |
|---|---|---|
| `delivery` | `email` (défaut), `sms`, `none` | Comment le signataire reçoit son invitation. Avec `none`, Signoui n'envoie rien : vous obtenez le lien par `sign-link` et l'ouvrez dans votre site |
| `otp_channel` | `sms` (défaut), `email`, `choice`, `none` | Comment la signature est confirmée. Avec `none`, aucun code : vous déclarez avoir identifié le signataire vous-même (`external_id` obligatoire). Lisez d'abord [ce que vous assumez](https://signoui.fr/documentation/#signature-sans-code--ce-que-vous-assumez) |
| `flow` | `standard` (défaut), `express` | Avec `express`, tout tient sur une page : le signataire arrive directement à la signature |

### Le parcours le plus court

Votre utilisateur est connecté chez vous, il a déjà consulté le document dans votre interface, il clique sur « Signer » :

```json
POST /envelopes/{id}/signers
{
  "full_name": "Jean Client",
  "email": "jean@exemple.fr",
  "external_id": "user-42",
  "delivery": "none",
  "otp_channel": "none",
  "flow": "express",
  "return_url": "https://www.monsite.fr/contrats/123"
}
```

Après l'envoi du dossier, demandez un lien avec `sign-link`, puis affichez-le dans une iframe ou redirigez-y votre utilisateur. Il voit une seule page : le document (qu'il peut faire défiler), son champ de signature (nom tapé ou tracé au doigt) et ses éventuels autres champs, la case de consentement, et un bouton **« Signer le document »**. Un clic, c'est signé, et le webhook `signer.signed` part aussitôt.

**Ce que le dossier de preuve contient toujours dans ce parcours** : l'identité déclarée et votre `external_id`, les pages consultées et le temps passé, le texte et la version du consentement coché, l'adresse IP et le navigateur, l'horodatage, l'empreinte SHA-256 du document, puis le scellement PAdES et l'ancrage temporel.

**Ce qu'il ne contient plus** : la preuve que le document a été lu jusqu'à la dernière page avant de pouvoir signer, et le code reçu sur un second canal.

### Express avec code : le compromis recommandé

Vous pouvez simplifier le parcours tout en gardant un code : `flow: "express"` avec `otp_channel: "sms"` affiche la même page unique, dont le bouton devient « Signer avec un code », puis l'écran du code SMS. C'est le bon choix quand le document engage vraiment (contrat, mandat) : une seule page, mais une identification par deux canaux.

### POST /envelopes/{id}/signers/{signer_id}/sign-link

Corps facultatif : durée de validité de 5 minutes à 24 heures (60 minutes par défaut) et page de retour.

```json
{ "ttl_minutes": 60, "return_url": "https://www.monsite.fr/merci" }
```

Réponse :

```json
{
  "url": "https://app.signoui.fr/sign/…",
  "expires_at": "2026-10-08T10:12:00+00:00",
  "signer_id": "sgn_…",
  "mode": "embedded"
}
```

Conditions : le dossier est envoyé (`pending`) et c'est le tour de ce signataire (en circuit séquentiel). Chaque appel génère un **nouveau lien et rend le précédent inutilisable** ; le lien envoyé par e-mail, s'il existe, reste valable. Le lien est personnel : générez-le au moment où votre client est connecté chez vous, et ne le transmettez pas ailleurs. Il fonctionne quels que soient `delivery` et `flow`.

### Dans une iframe

Déclarez d'abord vos domaines dans l'espace client, rubrique **Paramètres**, « Signature intégrée : domaines autorisés » (par exemple `https://www.monsite.fr`, un par ligne). Sans cela, les navigateurs refusent d'afficher la page de signature dans une iframe. Ensuite :

```html
<iframe src="URL_RETOURNÉE_PAR_SIGN_LINK" title="Signature du document"
        style="width:100%;height:760px;border:0"></iframe>
<script>
  window.addEventListener("message", function (e) {
    if (e.origin !== "https://app.signoui.fr") return; // n'écoutez que Signoui
    if (e.data && e.data.type === "signoui:signed") {
      // e.data.envelope_id, e.data.signer_id,
      // e.data.envelope_status : "pending" ou "completed"
      // Masquez l'iframe et affichez votre confirmation.
    }
  });
</script>
```

Sur mobile, affichez l'iframe en pleine largeur (la page est conçue à partir de 390 pixels) ou préférez la redirection. La page de signature fonctionne sans cookie tiers, y compris dans Safari et en navigation privée.

### Par redirection

Redirigez votre client vers l'URL obtenue. À la fin, si un `return_url` est défini (à la création du signataire ou dans l'appel `sign-link`), un bouton « Revenir sur le site » s'affiche et le retour se fait automatiquement après 2,5 secondes.

**Ne considérez pas le retour sur votre site comme la preuve de la signature** : votre client peut fermer la page avant. La source de vérité est le webhook `signer.signed` (ou `GET /envelopes/{id}`).

### Signature sans code : ce que vous assumez

Avec `otp_channel: "none"`, Signoui ne vérifie plus l'identité du signataire par un second canal : c'est **vous** qui l'avez identifié, par son compte chez vous. La preuve le dit explicitement : `identification: "delegated_to_sender"` et votre `external_id` dans le journal, mention « identification déléguée à l'émetteur » sur le certificat. Le consentement, l'horodatage, l'adresse IP et le scellement restent identiques, ainsi que la lecture complète du document en parcours `standard`.

En cas de contestation, la solidité de la signature dépend alors de votre propre authentification (mot de passe seul, double authentification…), que vous devrez pouvoir démontrer. C'est un choix courant pour des devis et contrats entre un professionnel et un client déjà identifié. Pour les engagements importants, gardez le code SMS (`otp_channel: "sms"`), qui fonctionne aussi en signature intégrée. Cette option est encadrée par nos [conditions générales d'utilisation](https://signoui.fr/cgu/).

**Aucun message de Signoui.** Avec `otp_channel: "none"`, le signataire n'a aucun contact avec Signoui : ni invitation, ni code, ni e-mail de confirmation une fois le document scellé. Et si tous les signataires du dossier sont en `none`, Signoui ne vous envoie pas d'e-mail non plus (ni « signé », ni « refusé », ni « expiré ») : votre plateforme est tenue au courant par les webhooks `envelope.completed`, `envelope.declined` et `envelope.expired`, et par `GET /envelopes/{id}`.

C'est donc à vous de remettre le PDF signé au signataire : à réception de `envelope.completed`, téléchargez le document `sealed` et transmettez-le-lui (votre espace client, votre propre e-mail). L'`email` du signataire reste obligatoire, car il figure dans le dossier de preuve. Le dossier de preuve est aussi complet que pour tout autre envoi. Dès qu'un signataire du dossier a un code (SMS ou e-mail), les e-mails habituels reprennent.

## Idempotence

Sur `POST /envelopes` et `POST /envelopes/{id}/send`, passez un en-tête `Idempotency-Key` : une chaîne unique de votre choix, par exemple votre numéro de devis. Rejouer la même requête dans les 24 heures renvoie la réponse d'origine, sans rien recréer ni renvoyer. Réutiliser la même clé avec un corps différent donne une erreur `409 idempotency_mismatch`.

C'est ce qui vous permet de relancer sans risque un appel qui a échoué sur un problème réseau : jamais deux dossiers, jamais deux invitations.

## Erreurs

Toutes les erreurs ont la même forme :

```json
{
  "error": {
    "code": "credits_insufficient",
    "message": "Solde insuffisant : 2 envois nécessaires (un par signataire), 1 disponible. Achetez un pack d'envois.",
    "param": null
  }
}
```

| HTTP | `code` | Signification |
|---|---|---|
| 401 | `missing_api_key`, `invalid_api_key` | Clé absente, inconnue ou révoquée |
| 402 | `credits_insufficient`, `organization_suspended` | Solde d'envois insuffisant ; organisation suspendue |
| 403 | `insufficient_scope` | La clé n'a pas la portée requise |
| 404 | `not_found`, `document_not_found` | Ressource inexistante, ou appartenant à une autre organisation (même réponse, volontairement) |
| 409 | `invalid_transition`, `signer_duplicate`, `rank_taken`, `idempotency_mismatch` | Action impossible dans l'état actuel |
| 422 | `validation_error`, `document_missing`, `signer_missing`, `signature_field_missing` | Entrée invalide (`param` indique le champ en cause) |
| 429 | `rate_limited` | Trop de requêtes : attendez une minute avant de réessayer |

## Bonnes pratiques

- **Vérifiez ce qui a été signé.** Gardez le `sha256` du document envoyé et comparez-le à `data.document_sha256` de l'événement `signer.signed` du journal : c'est la preuve que le document signé est bien celui que vous avez envoyé.
- **Archivez la preuve chez vous.** Téléchargez l'archive `evidence` dès `envelope.completed`. Signoui la conserve 10 ans, mais la preuve vous appartient.
- **Séparez test et production.** Une clé `sk_test_` en développement et en recette, une clé `sk_live_` distincte en production, chacune avec les seules portées nécessaires.
- **Préférez le code SMS** (`otp_channel: "sms"`) : un second canal, indépendant de l'e-mail, renforce l'identification du signataire.
- **Gardez la clé sur votre serveur**, jamais dans une page web ni une application mobile.
- **Rendez vos appels rejouables** avec `Idempotency-Key`, et traitez les webhooks en arrière-plan.

## Facturation mensuelle (comptes API)

Pour les volumes importants, votre organisation peut passer en **facturation mensuelle** : plus de packs à acheter, chaque envoi au-delà des 2 envois offerts du mois est facturé en fin de mois.

| Envoi | Prix HT |
|---|---|
| Envoi confirmé par un code SMS ou e-mail | 0,60 € |
| Envoi sans code (`otp_channel: "none"`, signataire identifié par vous) | 0,40 € |
| Minimum mensuel, dès qu'il y a au moins un envoi facturable | 150 € |

Un tarif sur mesure est possible selon votre volume. La facture est émise le 1er du mois pour le mois écoulé, avec une ligne par tarif. Elle est prélevée automatiquement si vous avez enregistré un moyen de paiement (carte ou mandat SEPA) dans la rubrique **Mes envois** de l'espace client ; sinon, elle vous est envoyée par e-mail avec un lien de paiement à 15 jours.

Sur un compte mensuel, `GET /me` renvoie `billing_mode: "api_monthly"` et `sends_available: null`, et [`GET /me/usage`](https://signoui.fr/documentation/#get-meusage) donne à tout moment les envois du mois et le montant estimé de la prochaine facture. Pour ouvrir un compte mensuel, [contactez-nous](https://signoui.fr/contact/).

## Limites

| Limite | Valeur |
|---|---|
| Taille du PDF | 20 Mo et 300 pages maximum, un PDF par dossier |
| `metadata` | 4 Ko maximum |
| Débit | 60 requêtes par minute et par clé |
| Délai de signature | 1 à 180 jours |
| Langue | E-mails, SMS et pages de signature en français |

Une question technique, un besoin de débit plus élevé ou un compte API facturé au mois ? [Écrivez-nous](https://signoui.fr/contact/), un développeur vous répond.
