Signer le lot
Autoriser le lot par l'attestation, le code et ce avec quoi le signataire signe, puis préparer et signer ses parcours par paquets de dix.
Le signataire a lu la formule de consentement et, quand le niveau en demande un, reçu son code. La signature se fait en trois temps :
- autoriser : l'attestation, le code, et ce avec quoi le signataire signe — une clé par parcours, ou son certificat qualifié ;
- préparer, par dix : chaque parcours rend ce que la clé du signataire doit signer, document par document ;
- signer, par dix : la clé signe, et chaque parcours reçoit sa signature.
1Ce avec quoi le signataire signe
- Signature avancée (
SignatureLevel1) : pour chaque parcours du lot, une paire de clés ECDSA P-256 générée sur le poste ou l'appareil du signataire, non exportable ; sa clé publique au format SPKI (DER, encodé en base64). Une clé ne sert qu'à un parcours, et à une seule signature. - Signature qualifiée (
2) et avancée avec certificat qualifié (3) : le certificat qualifié du signataire, sur sa carte ou sa clé ; sa chaîne, chaque certificat en DER encodé en base64, le sien en tête. Votre logiciel fait signer sa clé par le système du poste, qui demande au signataire son code PIN. En signature qualifiée, le certificat doit être sur un dispositif qualifié (RequiresQualifiedDevice).
2Autoriser
POST https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Authorize
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{
"Code": "482913",
"ConsentAccepted": true,
"ConsentSha256": "9f2c5e…",
"Keys": [
{ "TransactionId": "cccccccc-0000-4000-8000-000000000001", "PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…" },
{ "TransactionId": "cccccccc-0000-4000-8000-000000000004", "PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…" }
]
}curl -X POST "https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Authorize" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"Code": "482913",
"ConsentAccepted": true,
"ConsentSha256": "9f2c5e…",
"Keys": [{"TransactionId": "cccccccc-0000-4000-8000-000000000001", "PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"}, {"TransactionId": "cccccccc-0000-4000-8000-000000000004", "PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"}]
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Post, "https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Authorize");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"Code": "482913",
"ConsentAccepted": true,
"ConsentSha256": "9f2c5e…",
"Keys": [
{
"TransactionId": "cccccccc-0000-4000-8000-000000000001",
"PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"
},
{
"TransactionId": "cccccccc-0000-4000-8000-000000000004",
"PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"
}
]
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Authorize", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"Code": "482913",
"ConsentAccepted": true,
"ConsentSha256": "9f2c5e…",
"Keys": [{"TransactionId": "cccccccc-0000-4000-8000-000000000001", "PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"}, {"TransactionId": "cccccccc-0000-4000-8000-000000000004", "PublicKeySpki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"}]
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Code: le code saisi par le signataire (les espaces sont ignorés) ; vide quand le niveau n'en demande pas.ConsentAccepted:true, le signataire a accepté la formule.ConsentSha256: l'empreinte SHA-256, en hexadécimal, de la formule telle que vous l'avez montrée —ConsentText, chaque suite de blancs réduite à une espace, sans blanc au début ni à la fin, encodé en UTF-8.Keys(signature avancée) : une clé publique par parcours du lot, tous les parcours que le code autorise, toutes distinctes.CertificateChain(niveaux à certificat) : la chaîne du certificat, à la place deKeys:"CertificateChain": ["MIIF…", "MIIE…"].
Ce avec quoi le signataire signe est jugé avant que le code soit consommé — un certificat refusé ne le brûle pas. La réponse :
{
"BatchId": "abababab-0000-4000-8000-000000000009",
"TransactionIds": ["cccccccc-0000-4000-8000-000000000001", "cccccccc-0000-4000-8000-000000000004"],
"ExpiresAtUtc": "2026-11-09T11:16:40.000Z"
}Le lot reste autorisé une heure : le temps de préparer et de signer ses parcours.
3Préparer, par dix
POST https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Prepare
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{ "TransactionIds": ["cccccccc-0000-4000-8000-000000000001", "cccccccc-0000-4000-8000-000000000004"] }curl -X POST "https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Prepare" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"TransactionIds": ["cccccccc-0000-4000-8000-000000000001", "cccccccc-0000-4000-8000-000000000004"]
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Post, "https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Prepare");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"TransactionIds": [
"cccccccc-0000-4000-8000-000000000001",
"cccccccc-0000-4000-8000-000000000004"
]
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Prepare", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"TransactionIds": ["cccccccc-0000-4000-8000-000000000001", "cccccccc-0000-4000-8000-000000000004"]
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;{
"Prepared": [
{
"TransactionId": "cccccccc-0000-4000-8000-000000000001",
"PreparationToken": "q3Jd…",
"CertificatePem": "-----BEGIN CERTIFICATE-----\nMII…\n-----END CERTIFICATE-----",
"SignatureAlgorithm": "ECDSA",
"Items": [ { "TransactionFileId": "eeeeeeee-0000-4000-8000-000000000005", "SignedAttributes": "MWow…" } ],
"ExpiresAtUtc": "2026-11-09T10:27:40.000Z"
}
],
"Failed": [
{ "Id": "cccccccc-0000-4000-8000-000000000004", "Result": "Le document « Avenant » a changé depuis l'envoi du code : relisez-le avant de signer ce parcours" }
]
}Chaque parcours préparé rend son jeton (PreparationToken), le certificat du signataire, l'algorithme à employer (SignatureAlgorithm)
et un élément par document : SignedAttributes, en base64, ce que la clé doit signer. Failed dit les parcours écartés et pourquoi.
En signature avancée, le certificat est émis sur la clé du parcours et vaut une dizaine de minutes (ExpiresAtUtc) : signez chaque
paquet aussitôt préparé. Aux niveaux à certificat, la préparation vaut jusqu'à la fin du lot : préparez tout, faites signer le dispositif
en une fois, puis signez par paquets.
4Signer, par dix
POST https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Complete
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…
Content-Type: application/json
{
"Transactions": [
{
"TransactionId": "cccccccc-0000-4000-8000-000000000001",
"PreparationToken": "q3Jd…",
"Signatures": [ { "TransactionFileId": "eeeeeeee-0000-4000-8000-000000000005", "Signature": "o9Kc…" } ]
}
]
}curl -X POST "https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Complete" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" \
-H "Content-Type: application/json" \
-d '{
"Transactions": [{"TransactionId": "cccccccc-0000-4000-8000-000000000001", "PreparationToken": "q3Jd…", "Signatures": [{"TransactionFileId": "eeeeeeee-0000-4000-8000-000000000005", "Signature": "o9Kc…"}]}]
}'using HttpClient client = new();
using HttpRequestMessage request = new(HttpMethod.Post, "https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Complete");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = new StringContent("""
{
"Transactions": [
{
"TransactionId": "cccccccc-0000-4000-8000-000000000001",
"PreparationToken": "q3Jd…",
"Signatures": [
{
"TransactionFileId": "eeeeeeee-0000-4000-8000-000000000005",
"Signature": "o9Kc…"
}
]
}
]
}
""", Encoding.UTF8, "application/json");
using HttpResponseMessage response = await client.SendAsync(request);
string result = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(result);const response = await fetch("https://staging-api.docage.com/Transactions/ConsentBatches/abababab-0000-4000-8000-000000000009/Complete", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"Transactions": [{"TransactionId": "cccccccc-0000-4000-8000-000000000001", "PreparationToken": "q3Jd…", "Signatures": [{"TransactionFileId": "eeeeeeee-0000-4000-8000-000000000005", "Signature": "o9Kc…"}]}]
})
});
const text = await response.text();
if (!response.ok) throw new Error(text);
const result = text ? JSON.parse(text) : null;Signature est la signature, par la clé du parcours ou du certificat, des octets de SignedAttributes décodés du base64 — tels quels,
sans les hacher vous-même —, encodée en base64 : ECDSA avec SHA-256 (les 64 octets r‖s que rend WebCrypto, ou la forme DER), ou
RSA PKCS#1 v1.5 avec SHA-256 quand SignatureAlgorithm vaut RSA. Une signature par document préparé ; dix parcours au plus par
requête.
[ { "Id": "cccccccc-0000-4000-8000-000000000001", "Result": "Success" } ]Success, ou la raison, parcours par parcours. Chaque document reçoit alors sa signature, horodatée, et le parcours continue : actions
suivantes, emails, clôture.
En JavaScript (signature avancée)
Dans l'application du signataire — jamais sur un serveur qui signerait à sa place. Ce code tourne tel quel dans Node.js ou une
application de bureau ; dans une page web d'un autre domaine, que l'API ne sert pas, faites passer appeler par votre serveur, les
clés restant dans le navigateur :
const api = 'https://staging-api.docage.com';
const appeler = async (methode, route, corps) => {
const reponse = await fetch(`$https://staging-api.docage.com/${route}`, {
method: methode,
headers: { Authorization: `Bearer ${jeton}`, 'Content-Type': 'application/json' },
body: JSON.stringify(corps ?? {}),
});
if (!reponse.ok) throw new Error(`${reponse.status} ${await reponse.text()}`);
return reponse.json();
};
const base64 = octets => btoa(String.fromCharCode(...new Uint8Array(octets)));
const octetsDe = texte => Uint8Array.from(atob(texte), c => c.charCodeAt(0));
const paquets = (liste, taille) => Array.from({ length: Math.ceil(liste.length / taille) }, (_, i) => liste.slice(i * taille, (i + 1) * taille));
// jeton : celui de la session du signataire ; lot : la réponse de ConsentBatches ; envoi : celle de SendCode ;
// code : saisi par le signataire, qui a accepté lot.ConsentText
const cles = new Map();
const Keys = [];
for (const id of envoi.TransactionIds) {
const paire = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
cles.set(id, paire);
Keys.push({ TransactionId: id, PublicKeySpki: base64(await crypto.subtle.exportKey('spki', paire.publicKey)) });
}
const formule = lot.ConsentText.replace(/\s+/g, ' ').trim();
const empreinte = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(formule)));
const ConsentSha256 = [...empreinte].map(o => o.toString(16).padStart(2, '0')).join('');
const route = `Transactions/ConsentBatches/${lot.BatchId}`;
const autorisation = await appeler('POST', `${route}/Authorize`, { Code: code, ConsentAccepted: true, ConsentSha256, Keys });
for (const TransactionIds of paquets(autorisation.TransactionIds, 10)) {
const preparation = await appeler('POST', `${route}/Prepare`, { TransactionIds });
const Transactions = [];
for (const parcours of preparation.Prepared) {
const Signatures = [];
for (const element of parcours.Items) {
const signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' },
cles.get(parcours.TransactionId).privateKey, octetsDe(element.SignedAttributes));
Signatures.push({ TransactionFileId: element.TransactionFileId, Signature: base64(signature) });
}
Transactions.push({ TransactionId: parcours.TransactionId, PreparationToken: parcours.PreparationToken, Signatures });
}
const resultats = await appeler('POST', `${route}/Complete`, { Transactions });
// resultats : « Success », ou la raison, parcours par parcours ; preparation.Failed : les parcours écartés
}En C#, la même clé se crée par ECDsa.Create(ECCurve.NamedCurves.nistP256), sa clé publique par ExportSubjectPublicKeyInfo(), et la
signature par SignData(octets, HashAlgorithmName.SHA256, DSASignatureFormat.IeeeP1363FixedFieldConcatenation) ; avec un certificat
qualifié, certificat.GetRSAPrivateKey().SignData(octets, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1).
Les refus
À l'autorisation, 400 :
| Message | Ce qu'il faut faire |
|---|---|
| « Le code de sécurité est incorrect » | Le ressaisir : cinq essais au plus. |
| « Trop de tentatives : demandez un nouveau code de sécurité » | Renvoyer un code. |
| « Le code de sécurité a expiré, demandez-en un nouveau » | Renvoyer un code. |
| « La formule de consentement affichée n'est pas celle du service : rouvrez la signature avant de signer » | Montrer ConsentText tel qu'il a été rendu, et en recalculer l'empreinte. |
| « Chaque parcours du lot se signe avec sa propre clé : une clé publique distincte par parcours est attendue » | Une clé neuve par parcours, pour tous les parcours du lot. |
| « Le certificat présenté n'est pas au nom de … » | Le certificat du signataire lui-même. |
| « Le certificat présenté ne permet pas une signature qualifiée : … » | Un certificat qualifié sur dispositif qualifié. |
| « Ce lot est déjà autorisé » | Le lot s'est déjà autorisé : préparez et signez ses parcours. |
À la préparation et à la signature, la raison d'un parcours dans Failed ou dans Result : « Le document « … » a changé depuis l'envoi
du code : relisez-le avant de signer ce parcours », « La signature reçue ne correspond pas au certificat du signataire », « Le jeton de
préparation est invalide », « La préparation de la signature a expiré : demandez un nouveau code »…
Les pièges
- Vérifiez vos clés avant d'autoriser : une clé publique illisible n'est refusée qu'à la préparation, le code déjà consommé — il faut alors signer ce parcours depuis son invitation.
- Un document modifié depuis le relevé — par la signature d'un autre participant, par exemple — fait écarter son parcours.
- Signez chaque parcours préparé avant son
ExpiresAtUtc. Une signature refusée ou un jeton invalide laissent la préparation en place : vous pouvez recommencer jusqu'à cette échéance. - La preuve garde l'adresse que la requête annonce (la première de
X-Forwarded-For, sinon celle de la connexion), son navigateur et sa langue : un serveur relais transmet l'adresse du signataire dansX-Forwarded-For, sinon c'est la sienne que la preuve garde. - Un navigateur ne peut pas appeler l'API depuis une page d'un autre domaine : la signature se fait dans l'application du signataire, ou par votre propre serveur relais — les clés restant chez le signataire.