Digital signature
Get a PDF signed with a digital signature: the SDK certifies the PDF with a certificate, then sends it to the signer. It is the recommended signature mode.
Prerequisites
- The signature module for your language:
SecureExchanges.Signature.NetCoreon .NET 8 (SecureExchanges.Signatureon .NET Framework),secure-exchanges-sdk[signature]in Python; it is included in the PHP package. See Choose your package. - A Chromium browser on the machine that certifies: the SDK drives it headless.
- .NET: see Docker and Linux for a Linux server or a container.
- Python:
playwright install chromium, once per machine (about 150 MB). - PHP: Chrome or Chromium installed; outside the usual locations, give its path in
SE_CHROME_PATH.
- Your credentials loaded: see Credentials and environments.
The six steps
- Read the PDF and compute its SHA-512 hash. It identifies the file in the next steps.
- Place the zones to sign with
ZoneBuilder: signature, date, initials, fields. See Signature zones. - Assign the zones to the signer, for this file.
- Get the document's certification token with
GetCertificationToken. - Certify the PDF with
CertifyPkiPdffromPkiFileHelper. - Send the certified PDF with
MultiRecipientMessage, passing it inCertifiedPdfs.
Complete example
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SecureExchangesSDK.Helpers;
using SecureExchangesSDK.Models;
using SecureExchangesSDK.Models.Args;
using SecureExchangesSDK.Models.Entity;
using SecureExchangesSDK.Models.JSON;
using SecureExchangesSDK.Models.Transport;
using SecureExchangesSDK.SecureExchanges;
using SecureExchangesSignatureSDK.Helpers;
using SecureExchangesSignatureSDK.Models.Args;
internal static class PkiCertification
{
private static async Task SendForSignature(string pdfPath, string signer)
{
// Your credentials come from your secret store, never from code.
Guid serial = ReadSecret("SecureExchanges:Serial");
Guid apiUser = ReadSecret("SecureExchanges:ApiUser");
Guid apiPassword = ReadSecret("SecureExchanges:ApiPassword");
var endpoint = new Uri("https://www.secure-exchanges.com/_api/0217/0217.asmx");
// 1. Read the PDF and compute its SHA-512 hash: it identifies the file in the next steps.
byte[] pdf = File.ReadAllBytes(pdfPath);
string sha512 = CryptoHelper.GetSHA512HashOfBytes(pdf);
// 2. Place the zones to sign (see "Signature zones").
SignZoneDetails[] zones;
using (var zoneBuilder = new ZoneBuilder(pdf, isPki: true))
{
zones = new[]
{
zoneBuilder.CreateFieldSignZone(page: 1, fieldName: "signature1", x: 60, y: 400, width: 200, height: 50),
zoneBuilder.CreateFieldDateSignZone(page: 1, fieldName: "date1", x: 60, y: 500, width: 100, height: 25),
};
}
// 3. Assign the zones to the signer.
var zonesBySigner = new List<RecipientZoneDefinition>
{
new RecipientZoneDefinition
{
Email = signer,
ZonesDefByFile = new[]
{
new FileZoneDefinition
{
UniqueName = sha512,
IsPKIFile = true,
ZonesDef = new SignZoneDefinition { Mode = "define", Zones = zones }
}
}
}
};
// 4. Get the document's certification token.
var token = SignHelper.GetCertificationToken(new GetCertificationTokenSDKArgs(
endpoint, serial, apiUser, apiPassword, sha512, CertificationTokenType.CertifyDocument, "en-CA"));
if (token.Status != 200)
throw new InvalidOperationException($"Certification token refused, status {token.Status}");
// 5. Certify the PDF. PkiFileHelper drives a headless browser: release it after use.
CertifiedPdfContainer certifiedPdf;
await using (var pki = new PkiFileHelper(isPreview: false, culture: "en-CA"))
{
certifiedPdf = await pki.CertifyPkiPdf(new CertifyPdfArgs
{
File = new FileArgs(pdf, Path.GetFileName(pdfPath), "application/pdf"),
Recipients = new List<string> { signer },
CertifyData = token.CertificationData,
RecipientZoneDefinitions = zonesBySigner
});
}
// 6. Send the certified PDF to the signer.
var args = new MutliRecipientArgs(
endPointUri: endpoint, serial: serial, user: apiUser, psw: apiPassword,
recipients: new List<RecipientInfo> { new RecipientInfo { Email = signer } },
message: "Please sign the attached document.",
subject: "Document to sign",
password: null, filesList: null,
sendMode: SendMethodEnum.onlyEmail, getBackHtml: false,
showSubject: true, getNotify: true, cultureID: "en-CA",
maximumOpenTime: 5, minutesExpiration: 10080);
args.CertifiedPdfs = new List<CertifiedPdfContainer> { certifiedPdf };
args.OwnerDontNeedToSign = true; // otherwise you sign too, last: false by default in .NET
var response = MessageHelper.MultiRecipientMessage(args);
if (response.Status != 200)
throw new InvalidOperationException($"Sending refused, status {response.Status}");
}
private static Guid ReadSecret(string name)
{
// Replace with a read from your secret manager (Azure Key Vault, etc.).
throw new NotImplementedException(name);
}
}
Note
On .NET Framework (SecureExchanges.Signature package), release PkiFileHelper with using rather than await using.
Good to know
| Point | Detail |
|---|---|
| The same PDF at every step | The SHA-512 hash identifies the file: do not modify it between computing the hash and certifying. |
| Preview | Set isPreview to true in your test configuration, with your preview credentials. It must follow the same environment as your other calls: true with the preview configuration, false in production. |
| Fields already in the PDF | With DetectExistingFields (true by default), they are detected; DefineRecipients lets you distribute them among signers. |
| Resources | PkiFileHelper starts a browser: release it as soon as certification is done, as in the example. |
| Several signers | Add one zone definition per signer, and every signer to the recipient list, in signing order: see Several signers. |
| Options without PKI | DoNotEncryptPDF and DoNotAppendCertificateToFile are deprecated and need not be used. See Move to digital signature. |
Several signers
Signatures happen one after the other, in the order of the recipient list:
- The first signer receives the email as soon as the message is sent.
- Each following signer receives it once the previous one has signed.
- When everything is signed, the signers and the licence owner receive the signed documents.
There is no parallel sending: the order of the list is the signing order. To notify people who do not sign, see Copy recipients.
The licence owner
The owner of the licence that sends the message is always added last to the signing chain. OwnerDontNeedToSign decides whether they must sign:
| Value | Effect |
|---|---|
| true | They do not have to sign: they only receive the signed documents at the end. |
| false | They sign last, after everyone else. They then need their own zones, otherwise the send is refused. |
Warning
The default value is not the same everywhere: false in .NET, true in Python and PHP. Always set it explicitly, as in the example.
Signing options
SignatureOptions, of type SignOptions, sets three aspects of the signing chain: the signing modes offered, a file the signer must attach, and copy recipients who do not sign. It only applies to a send with documents to sign. The example takes up step 6 of the complete example:
using System;
using System.Collections.Generic;
using SecureExchangesSDK.Helpers;
using SecureExchangesSDK.Models;
using SecureExchangesSDK.Models.Args;
using SecureExchangesSDK.Models.Entity;
using SecureExchangesSDK.Models.Enum;
using SecureExchangesSDK.Models.JSON;
using SecureExchangesSDK.Models.Transport;
using SecureExchangesSDK.SecureExchanges;
internal static class SigningOptions
{
// Step 6 of the complete example, with the signing options: certifiedPdf comes from step 5.
private static void SendWithOptions(CertifiedPdfContainer certifiedPdf, string signer,
string copyTo, string blindCopyTo)
{
// Your credentials come from your secret store, never from code.
Guid serial = ReadSecret("SecureExchanges:Serial");
Guid apiUser = ReadSecret("SecureExchanges:ApiUser");
Guid apiPassword = ReadSecret("SecureExchanges:ApiPassword");
var endpoint = new Uri("https://www.secure-exchanges.com/_api/0217/0217.asmx");
var options = new SignOptions
{
// Mode allowed for the signature and the initials: here, drawing only.
SignatureRestriction = SignatureModeRestriction.Trace,
// The signer must attach a file to finish: a single signer in this case.
UploadFileOptions = new SignOptionsFileUpload
{
UploadMode = ReplyFileUploadMode.UploadMandatory,
AuthorizedExtensions = new[] { ".pdf", ".jpg", ".png" },
Messages = new List<ReplyOptionsMessage>
{
new ReplyOptionsMessage { CultureID = "en-CA", Message = "Attach the completed Schedule B." },
new ReplyOptionsMessage { CultureID = "fr-CA", Message = "Joignez l'annexe B remplie." }
},
NoEditor = true
},
// Notified when the message is sent, they receive the signed documents at the end, without signing.
CCRecipients = new List<Recipient> { new Recipient { Email = copyTo } },
// Like the previous ones, but hidden from the signers.
BCCRecipients = new List<Recipient> { new Recipient { Email = blindCopyTo } }
};
var args = new MutliRecipientArgs(
endPointUri: endpoint, serial: serial, user: apiUser, psw: apiPassword,
recipients: new List<RecipientInfo> { new RecipientInfo { Email = signer } },
message: "Please sign the attached document, then attach the completed Schedule B.",
subject: "Document to sign",
password: null, filesList: null,
sendMode: SendMethodEnum.onlyEmail, getBackHtml: false,
showSubject: true, getNotify: true, cultureID: "en-CA",
maximumOpenTime: 5, minutesExpiration: 10080);
args.CertifiedPdfs = new List<CertifiedPdfContainer> { certifiedPdf };
args.SignatureOptions = options;
args.OwnerDontNeedToSign = true; // otherwise you sign too, last: false by default in .NET
var response = MessageHelper.MultiRecipientMessage(args);
if (response.Status != 200)
throw new InvalidOperationException($"Sending refused, status {response.Status}");
}
private static Guid ReadSecret(string name)
{
// Replace with a read from your secret manager (Azure Key Vault, etc.).
throw new NotImplementedException(name);
}
}
Allowed signing modes
SignatureRestriction sets the modes the signer may use for their signature and initials. The seal, always an image, is not affected. The restriction applies to every signer in the chain.
| Value | Mode offered |
|---|---|
NoRestriction |
All modes. This is the default value. |
Text |
Type their name: it is rendered in cursive writing. |
Trace |
Draw their signature with the mouse or a finger. |
Picture |
Upload an image of their signature, PNG or JPEG, 500 KB at most. |
Values combine with the | operator: Text and Trace together offer typing and drawing.
In PHP, combine their ->value with the | operator: SignatureModeRestriction::TEXT->value | SignatureModeRestriction::TRACE->value offers typing and drawing. A value that matches no mode is refused before sending.
File to attach
UploadFileOptions takes a SignOptionsFileUpload, which asks the signer to attach files to their signed documents.
| Property | Role |
|---|---|
UploadMode |
NoUpload, the default value: no file can be attached. UploadOptional: the signer may attach files. UploadMandatory: they cannot finish without attaching at least one. |
AuthorizedExtensions |
Allowed extensions, with the dot, for example .pdf. A file with another extension is refused; the signed documents are not affected. Without a list, any extension is allowed. |
Messages |
The instruction shown to the signer: one ReplyOptionsMessage per culture. Only the instruction whose culture is exactly the page's, fr-CA or en-CA, is shown: provide both. Write plain text. |
NoEditor |
true: the signer cannot write a message, they only attach files. |
NoReply |
No effect when signing. |
Messages also accepts dictionaries in Python, and arrays in PHP, with the CultureID and Message keys.
Important
UploadOptional and UploadMandatory require a single signer. With several recipients, the send is refused.
Copy recipients
CCRecipients and BCCRecipients add recipients who do not sign:
- As soon as the message is sent, they are notified that documents are to be signed. They cannot sign them.
- When everything is signed, they receive the signed documents.
Those in BCCRecipients are hidden: the signers do not see them, and the final email does not name them.
Each copy recipient has an email address, a phone number, or both. With a number only, they are reached by SMS; with both, they receive the email and a code by SMS.
In .NET, they are Recipient objects from the SecureExchangesSDK.SecureExchanges namespace: only Email and Phone matter.
In Python, they are Recipient objects, exported by secure_exchanges_sdk.models.entity.
In PHP, they are RecipientInfo objects, as for signers.
Common errors
| Symptom | Likely cause | Solution |
|---|---|---|
| The browser fails to start | Chromium missing or system dependencies missing | See Docker and Linux. |
| Send refused: "Not all recipients have fields defined for all files" | OwnerDontNeedToSign set to false, the .NET default, with no zones for the owner |
Set it to true, or add their zones. |
| Send refused, message starting with "The signature file upload mode" | UploadOptional or UploadMandatory with several signers |
Keep a single signer, or set UploadMode to NoUpload. |
| Certification token refused | Credentials, environment or licence | See Status codes and Licence and quotas. |
| Zones do not appear | Zones assigned to another file or another address | Check the SHA-512 hash and the signer's address. |
Next
- Signature zones
- Logs: track opening and signature
- Azure Functions