Contracten ondertekenen vanuit SharePoint met Oodrive en DocuSign: zo bouwden we PDF Sign in Shareflex

Een contract is pas klaar als het getekend is. In veel organisaties betekent dat nog steeds: de pdf downloaden uit SharePoint, uploaden naar een onderteken­portaal, de ondertekenaars opnieuw intypen, wachten, de getekende versie downloaden en weer op de juiste plek zetten. Voor een klant in de Benelux haalden we al die stappen weg. In hun module voor contractmanagement in SharePoint op Shareflex selecteert een gebruiker een pdf op het tabblad Documents van het contract en stuurt die ter ondertekening via Oodrive Sign of DocuSign – zonder SharePoint te verlaten.

In dit artikel laten we zien hoe we het bouwden: de gebruikersflow, de architectuur en echte TypeScript-code voor beide providers.

De gebruikersflow

Alles begint bij een menu PDF Sign op het tabblad Documents van een contract. Alleen de acties die zinvol zijn voor de geselecteerde documenten staan aan.

Het PDF Sign-menu op het tabblad Documents van een contract: opties gaan aan en uit op basis van de geselecteerde documenten.
Het PDF Sign-menu op het tabblad Documents van een contract: opties gaan aan en uit op basis van de geselecteerde documenten.
  1. Set Recipients – kies de provider en vul maximaal vier ondertekenaars in.
  2. Submit PDF for Sign – het document gaat naar Oodrive of DocuSign en de ondertekenaars krijgen hun e-mail.
  3. Collect Sign Status – zie wie al getekend heeft, bijvoorbeeld “PDF Signed by …, 1 of 2”.
  4. Download Signed PDF en Download Sign Certificate – beide komen naast het origineel, in de eigen map van het contract.
Kies per document de ondertekenprovider: Oodrive of DocuSign.
Kies per document de ondertekenprovider: Oodrive of DocuSign.
Maximaal vier ondertekenaars, die tekenen in de volgorde waarin ze zijn ingevuld.
Maximaal vier ondertekenaars, die tekenen in de volgorde waarin ze zijn ingevuld.

Architectuur: de browser vraagt, de server doet het werk

De belangrijkste ontwerpkeuze: geen enkele API-sleutel komt in de browser. Het menu roept Oodrive of DocuSign niet zelf aan. Het zet alleen een triggerveld op het document:

// Client side: the button only sets a trigger field - no API key ever reaches the browser
const results = await Promise.allSettled(itemIds.map(itemId =>
  SF.getListDataService().updateItem(args.contractWebInfo.webUrl, null, listUrl, itemId, {
    PDFSignSubmitRequested: '1',
    PDFSignSubmitStatus: 'Requested',
  }),
));
Eén bevestiging, daarna doet de server de rest.
Eén bevestiging, daarna doet de server de rest.

Een server-side Shareflex-rule op de documentenlijst pakt de wijziging op en doet het echte werk. Eén rule handelt alle vier de acties af:

// Server side: one ItemUpdated rule, four actions
async function main() {
  if (rules.Event.type !== 'ItemUpdated') return;
  const trigger = await rules.getItemById({
    listUrl: rules.Event.listUrl,
    itemId: rules.Event.itemId,
    fields: ['PDFSignSubmitRequested', 'PDFSignCollectStatusRequested', 'PDFSignDownloadRequested', 'PDFSignDownloadCertRequested'],
  });

  if (String(trigger?.PDFSignSubmitRequested ?? '').trim() === '1') {
    await submitPdfForSign();
  } else if (String(trigger?.PDFSignCollectStatusRequested ?? '').trim() === '1') {
    await collectSignStatus();
  } else if (String(trigger?.PDFSignDownloadRequested ?? '').trim() === '1') {
    await downloadSignedPdf();
  } else if (String(trigger?.PDFSignDownloadCertRequested ?? '').trim() === '1') {
    await downloadSignCertificate();
  }
}

Endpoints, sleutels, sjablonen, het afzenderadres en de e-mailteksten staan in een lijst PDF Sign Settings, met per provider één actief instellingenitem. Elk document onthoudt zijn eigen provider, zodat Oodrive en DocuSign naast elkaar gebruikt kunnen worden.

Het menu wordt elke keer opnieuw berekend als het opengaat, zodat Submit alleen beschikbaar is als er ondertekenaars zijn ingevuld, en de downloads pas als iedereen getekend heeft:

subMenuProps: {
  items: [ /* Set Recipients, Submit PDF for Sign, Collect Sign Status, Download Signed PDF, Download Sign Certificate */ ],
  // Recomputed every time the menu opens, so the options always match the current selection
  onRenderMenuList: (menuListProps, defaultRender) => {
    const selectedItems = getSelectedDocumentItems(SF);
    const disabledByKey: Record<string, boolean> = {
      PDFSignSubmit: !selectedItems.some(hasFilledRecipient),
      PDFSignCollectStatus: !selectedItems.some(hasBeenSubmittedForSign),
      PDFSignDownloadSigned: !selectedItems.some(hasCompletedSignature),
      PDFSignDownloadCertificate: !selectedItems.some(hasCompletedSignature),
    };
    menuListProps.items.forEach(item => {
      if (item.key in disabledByKey) item.disabled = disabledByKey[item.key];
    });
    return defaultRender(menuListProps);
  },
},

Oodrive Sign in vijf aanroepen

Voor Oodrive maakt de rule een contract aan, uploadt de pdf, maakt de ondertekenaars aan als Oodrive-contacten (of vindt ze terug), koppelt ze in ondertekenvolgorde en start de transactie – en dat laatste verstuurt de e-mails.

// Oodrive Sign: 1. create contract  2. upload PDF  3. create/match recipients
//               4. attach recipients in signing order  5. start the transaction
const createContractResponse = await rules.httpService({
  connectionId: connection.id,
  url: `${apiBaseUrl}/contracts`,
  method: 'POST',
  body: JSON.stringify({
    name: fileName,
    contract_definition_id: contractDefinitionId,
    vendor_email: senderEmail,
    auto_close: 1,   // countersign automatically once everyone has signed
  }),
});
// ...upload, recipients...
const attachBody = recipientIds.map((recipientId, index) => ({
  recipient_id: Number(recipientId) || recipientId,
  signature_mode: signatureMode,
  rank: index + 1,   // recipients sign in the order they were entered
}));
await rules.httpService({ connectionId: connection.id, url: `${apiBaseUrl}/contracts/${contractId}/recipients`,
  method: 'POST', body: JSON.stringify(attachBody) });

// This is what actually sends the notification e-mails
await rules.httpService({ connectionId: connection.id, url: `${apiBaseUrl}/contracts/${contractId}/transaction/start`, method: 'POST' });

Een les uit de praktijk: de multipart-upload. De standaard FormData-helper leverde een verzoek op waarin Oodrive de boundary niet kon vinden, dus bouwen we de multipart-body zelf op met een vaste boundary:

// Built manually: the SDK's FormData helper produced a boundary Oodrive's parser could not find
// ("Multipart: Boundary not found"), so the same fixed boundary is used in header and body.
const boundary = `LialisPDFSignBoundary${Date.now()}`;
const multipartBody = buildMultipartBody(boundary, fileName, fileBytes);

await rules.httpService({
  connectionId: connection.id,
  url: `${apiBaseUrl}/contracts/${contractId}/document`,
  method: 'POST',
  headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },
  body: multipartBody,
  timeoutInSeconds: 120,
});

DocuSign: envelope, routering en verankerde handtekeningvelden

Voor DocuSign maakt dezelfde knop een envelope aan met het contract plus een standaard handtekeningpagina, voegt de ondertekenaars toe met sequentiële routering (dezelfde volgorde als bij Oodrive), plaatst voor elke ondertekenaar een handtekeningveld via een ankertekst en verstuurt de envelope.

// DocuSign: envelope with the contract + a signature page, sequential routing, anchored tabs, send
const createResponse = await rules.httpService({
  connectionId: connection.id,
  url: `${baseUri}/restapi/v2.1/accounts/${accountId}/envelopes`,
  method: 'POST',
  body: JSON.stringify({
    status: 'created',
    emailSubject: notificationTitle,
    emailBlurb: notificationBody,
    documents: [
      { documentId: '1', name: fileName, fileExtension: 'pdf', documentBase64: base64Encode(fileBytes) },
      { documentId: '2', name: 'Signature Page', fileExtension: 'pdf', documentBase64: DOCUSIGN_SIGNATURE_PAGE_PDF_BASE64 },
    ],
  }),
});

const signers = recipients.map((recipient, index) => ({
  email: recipient.email,
  name: `${recipient.firstName} ${recipient.lastName}`,
  recipientId: String(index + 1),
  routingOrder: index + 1,
}));
// ...PUT recipients, then per signer:
body: JSON.stringify({ signHereTabs: [{ anchorString: `/sig${signer.recipientId}/` }] }),
// ...and finally PUT { status: 'sent' }

De authenticatie vroeg om wat denkwerk. Shareflex-rules draaien in een V8-engine, niet in Node.js, dus de gebruikelijke JWT-flow met de crypto-bibliotheek van Node was niet beschikbaar. We gebruiken daarom de Authorization Code Grant met een refresh token. Omdat DocuSign een refresh token ongeldig maakt zodra het gebruikt is, wordt het nieuwe token direct opgeslagen:

// DocuSign Authorization Code Grant refresh - rules run in ClearScript/V8, not Node.js,
// so JWT signing with Node's crypto module is not available here.
const tokenResponse = await rules.httpService({
  connectionId: authConnection.id,
  url: `${apiBaseUrl}/oauth/token`,
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Basic ${basicAuth}` },
  body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,
});
// DocuSign invalidates the old refresh token immediately, so the new one is stored
// before anything later in the flow can fail.
await updateSettingsRefreshToken(settingsItem, newRefreshToken);

Status en getekende documenten terug in SharePoint

“Collect Sign Status” vraagt de provider wie er getekend heeft en zet dat om naar leesbare tekst in de lijst:

const signedNames: string[] = [];
for (const [index, recipient] of recipients.entries()) {
  const transactionRecipient = transactionRecipients.find(tr => Number(tr.rank) === index + 1);
  if (String(transactionRecipient?.signature_status ?? '').trim() === 'SIGNED') {
    signedNames.push(`${recipient.firstName} ${recipient.lastName}`);
  }
}
const status = signedNames.length === 0
  ? 'Submitted to Oodrive for signature'
  : `PDF Signed by ${formatNameList(signedNames)}, ${signedNames.length} of ${recipients.length}`
    + (signedNames.length === recipients.length ? ' - Completed' : '');
PDF Sign Supplier en PDF Sign Status als kolommen in de documentenlijst.
PDF Sign Supplier en PDF Sign Status als kolommen in de documentenlijst.

Heeft iedereen getekend, dan worden de getekende pdf en het ondertekeningscertificaat gedownload naar dezelfde contractmap als het origineel, als “…-SIGNED.pdf” en “…-certificate.pdf”. Zo staan ze direct bij het contract:

// The signed copy goes into the SAME contract folder as the original, so it appears on the contract
const signedFileName = `${originalFileName.replace(/\.pdf$/i, '')}-SIGNED.pdf`;
const listServerRelativeUrl = rules.Utils.makeServerRelative(rules.Event.listUrl);
const fileDirServerRelative = String(documentItem?.FileDirRef ?? '');
const folderPath = fileDirServerRelative.startsWith(listServerRelativeUrl)
  ? fileDirServerRelative.slice(listServerRelativeUrl.length)
  : '';

await rules.createFile({
  listUrl: rules.Event.listUrl,
  fileName: signedFileName,
  content: base64Encode(signedPdfBytes),
  folderPath,
  ensureFolderPath: true,
  encoding: 'Base64',
  overwrite: true,
  eventsActivated: false,
});

Elke stap wordt ook vastgelegd in een PDF Sign-logboek op het document, zodat support altijd kan zien wat er gebeurd is.

Wat het oplevert

  • Geen downloaden en uploaden meer: van contract naar getekend contract zonder SharePoint te verlaten.
  • Twee providers, één flow: Oodrive en DocuSign achter dezelfde knoppen.
  • Veilig: sleutels en tokens blijven server-side, in een instellingenlijst.
  • Ondertekenvolgorde: maximaal vier ondertekenaars, in de volgorde die u kiest.
  • Bewijs op de juiste plek: getekende pdf en certificaat worden bij het contract opgeslagen.
  • Transparant: een statuskolom en een logboek per document.

Wat we geleerd hebben

  • Laat de browser een vlag zetten en laat de server met externe API’s praten.
  • Ontwerp één gebruikersflow en verberg de verschillen tussen providers in de rule.
  • Sla refresh tokens direct op; ze zijn maar één keer bruikbaar.
  • Schrijf resultaten terug naast het brondocument, niet op een aparte plek.

Contracten ondertekenen vanuit SharePoint?

Digitaal ondertekenen is onderdeel van het contractmanagement dat we bouwen op Shareflex Contract. Lees ook over onze Shareflex DigitalSign API voor DocuSign, de AI PDF-scan en de functie om contracten te kopiëren in dezelfde module. Neem contact op voor een demo.