> ## Documentation Index
> Fetch the complete documentation index at: https://alpha.developer.tomorro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Full cycle example

> Connect your own signature tool: a working Yousign example with two Make scenarios

[Make](https://www.make.com) is a no-code automation platform. A **blueprint** is a Make scenario exported as JSON: import it (*Import blueprint*), connect your own accounts, and every module comes back configured.

The two blueprints below run every flow of this guide with Yousign: Tomorro → Yousign (Flows 1 and 3) and Yousign → Tomorro (Flows 2 and 4). Use them as a **head start**: import them if you run Make, read them as a reference implementation if you build on n8n, custom code or another signature tool.

<Card title="Get the 2 Make blueprints" icon="google-drive" href="https://drive.google.com/drive/folders/1YXxdcyQ7cBKlkSu5FGwl0nZx5Vk5-t0U">
  The *3rd party* files: Tomorro → Third-Party (Flows 1 and 3) and Third-Party → Tomorro (Flows 2 and 4).
</Card>

<Accordion title="Notes">
  The two Make blueprints above are a working example, not a ready-made integration. They make choices you may want to change for your own setup:

  * **Yousign sandbox**: every Yousign call targets `api-sandbox.yousign.app`. Switch to `api.yousign.app` for production.
  * **Signature level**: `electronic_signature` (simple electronic signature), with `signature_authentication_mode: no_otp`, so signers get no SMS or email code.
  * **Language and time zone**: signers, emails and audit trail in English (`locale: en`), time zone `Europe/Paris`.
  * **Delivery**: `delivery_mode: email`, Yousign sends the invitations and reminders to the signatories.
  * **Signature zones**: only Tomorro fields of type `signature` are sent, `initials` are ignored. A signatory with no zone in Tomorro gets one at the bottom of the last page of the main document.
  * **Webhook secrets**: the Tomorro and Yousign webhook secrets are read from a Make data store.
</Accordion>

## Yousign Full Demo

<Frame>
  <iframe src="https://www.loom.com/embed/e511cf7fd89645a4ba3a2b03d8b877dd" title="Third-party signature demo: Yousign and Make" width="100%" height="420" frameBorder="0" allowFullScreen />
</Frame>

### Flow 1 - \[Tomorro → Yousign] Signature is requested

<Steps>
  <Step title="Step 0 - Triggered by a user on Tomorro">
    Triggered when a user clicks "Send for signature" on a Tomorro contract set to the "Other signature tool" option (after the approval workflow if there is one).
  </Step>

  <Step title="Step 1 - Receive the signature.requested webhook">
    *Make module 1*

    Keep at least `data.contractId`, it is the key for every following call.
  </Step>

  <Step title="Step 1.1 - Verify the Leeway signature">
    *Make modules 2, 3*

    The secret is read from the Make data store, key `tomorro_webhook_secret`. Inputs: the raw body, the request headers and the secret.

    ```javascript theme={null}
    const crypto = require('crypto');
    const { rawBody, headers, secret } = input;

    const REPLAY_WINDOW_MS = 5 * 60 * 1000;
    const header = (headers || []).find((h) => ['leeway-signature', 'leeway_signature'].includes(String(h.name).toLowerCase()));
    if (!header) return { valid: false, reason: 'missing Leeway-Signature header' };

    const parts = Object.fromEntries(String(header.value).split(',').map((p) => p.trim().split('=')));
    const { t, sha256 } = parts;
    if (!t || !sha256) return { valid: false, reason: 'malformed Leeway-Signature header' };
    if (Math.abs(Date.now() - Number(t)) > REPLAY_WINDOW_MS) return { valid: false, reason: 'timestamp outside the 5 minute window' };

    const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
    const matches = expected.length === sha256.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sha256));
    if (!matches) return { valid: false, reason: 'signature mismatch' };

    return { valid: true, event: JSON.parse(rawBody) };
    ```
  </Step>

  <Step title="Step 1.2 - Route the event">
    *Make module 4*

    Continue only when `valid` is `true` and `test` is not `true`. Route on `eventType`: `signature.requested` → this flow, `signature.canceled` → Flow 3.
  </Step>

  <Step title="Step 2 - Retrieve the files and signature information">
    *Make module 5*

    ```http theme={null}
    GET https://api.tomorro.com/v2/contracts/{contractId}/signing-files
    x-api-key: <your api key>
    ```
  </Step>

  <Step title="Step 3 - Retrieve more contract information (optional)">
    *Make module 6*

    ```http theme={null}
    GET https://api.tomorro.com/v2/contracts/{contractId}
    x-api-key: <your api key>
    ```
  </Step>

  <Step title="Step 4 - Work in Yousign" />

  <Step title="Step 4.1 - Build the Yousign signature request">
    *Make module 7*

    Inputs: `contractId`, `contractName` and `signatureNote` from the webhook, `firstSigningParty` from step 2. `external_id` = `contractId` is the link used by Flows 2, 3 and 4.

    ```javascript theme={null}
    const { contractId, contractName, signatureNote, firstSigningParty } = input;

    const body = {
      name: String(contractName || 'Tomorro contract').slice(0, 128),
      delivery_mode: 'email',
      timezone: 'Europe/Paris',
      ordered_signers: Boolean(firstSigningParty),
      external_id: contractId,
      audit_trail_locale: 'en',
    };
    if (signatureNote) body.email_custom_note = String(signatureNote).slice(0, 500);

    return { body: JSON.stringify(body) };
    ```
  </Step>

  <Step title="Step 4.2 - Create the Yousign signature request">
    *Make module 8*

    ```http theme={null}
    POST https://api-sandbox.yousign.app/v3/signature_requests
    Authorization: Bearer <your yousign api key>
    Content-Type: application/json

    {
      "name": "<contractName>",
      "delivery_mode": "email",
      "timezone": "Europe/Paris",
      "ordered_signers": true,
      "external_id": "<contractId>",
      "audit_trail_locale": "en",
      "email_custom_note": "<signatureNote>"
    }
    ```
  </Step>

  <Step title="Step 4.3 - Download each file to sign">
    *Make modules 9, 10*

    Iterate over `data.files` from step 2, then download each `downloadUrl` (no auth header, valid 5 minutes).

    ```http theme={null}
    GET <files[].downloadUrl>
    ```
  </Step>

  <Step title="Step 4.4 - Upload each document to Yousign">
    *Make module 11*

    ```http theme={null}
    POST https://api-sandbox.yousign.app/v3/signature_requests/{signatureRequestId}/documents
    Authorization: Bearer <your yousign api key>
    Content-Type: multipart/form-data

    file   = <PDF from step 4.3>, filename <files[].filename>
    nature = signable_document
    ```
  </Step>

  <Step title="Step 4.5 - Map each Tomorro file to its Yousign document">
    *Make module 12*

    Aggregate one entry per file, joined with `,`:

    ```json theme={null}
    {"fileId":"<files[].id>","documentId":"<Yousign document id from step 4.4>"}
    ```
  </Step>

  <Step title="Step 4.6 - Build the Yousign signers">
    *Make module 13*

    Inputs: `data` from step 2 (`files`, `signatories`, `firstSigningParty`) and the mapping from step 4.5. Orders the signers, converts the Tomorro field positions into Yousign coordinates and cleans the names.

    ```javascript theme={null}
    const { signingFiles, documents } = input;

    const documentIdByFileId = Object.fromEntries(
      JSON.parse(`[${documents}]`).map(({ fileId, documentId }) => [fileId, documentId]),
    );
    const files = signingFiles.files || [];
    const signatories = signingFiles.signatories || [];

    // Yousign signs in creation order: members ranked by order, guests on the side given by firstSigningParty
    const members = signatories.filter((s) => s.type === 'member').sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
    const guests = signatories.filter((s) => s.type !== 'member');
    const ordered = signingFiles.firstSigningParty === 'guest' ? [...guests, ...members] : [...members, ...guests];

    // Yousign minimum signature field size
    const WIDTH = 85;
    const HEIGHT = 37;
    const clamp = (value, max) => Math.max(0, Math.min(Math.round(value), Math.floor(max)));
    const mainFile = files.find((f) => f.isMain) || files[0];

    const signers = ordered.map((signatory, index) => {
      const fields = files.flatMap((file) =>
        (file.fields || [])
          .filter((f) => f.signatoryId === signatory.id && f.type === 'signature' && documentIdByFileId[file.id])
          .map((f) => {
            const page = file.pages[f.pageIndex];
            return {
              document_id: documentIdByFileId[file.id],
              type: 'signature',
              page: f.pageIndex + 1,
              x: clamp((f.positionX / 100) * page.width, page.width - WIDTH),
              y: clamp((f.positionY / 100) * page.height, page.height - HEIGHT),
              width: WIDTH,
              height: HEIGHT,
            };
          }),
      );

      // No zone placed in Tomorro: Yousign needs one, so drop it at the bottom of the main document's last page
      if (!fields.length && mainFile) {
        const pageNumber = mainFile.pages.length;
        const page = mainFile.pages[pageNumber - 1];
        fields.push({
          document_id: documentIdByFileId[mainFile.id],
          type: 'signature',
          page: pageNumber,
          x: clamp(40 + (index % 4) * 130, page.width - WIDTH),
          y: clamp(page.height - 80 - Math.floor(index / 4) * 60, page.height - HEIGHT),
          width: WIDTH,
          height: HEIGHT,
        });
      }

      // name can be "" for a guest invited by email; Yousign rejects names with characters like . or _
      const clean = (value) => value.replace(/[^\p{L}0-9`'() -]/gu, ' ').replace(/\s+/g, ' ').trim();
      const fallback = clean(signatory.email.split('@')[0]) || 'Signataire';
      const [firstName, ...lastNames] = (clean(signatory.name || '') || fallback).split(' ');

      return {
        email: signatory.email,
        body: JSON.stringify({
          info: {
            first_name: firstName,
            last_name: lastNames.join(' ') || firstName,
            email: signatory.email,
            locale: 'en',
          },
          signature_level: 'electronic_signature',
          signature_authentication_mode: 'no_otp',
          fields,
        }),
      };
    });

    return { signers };
    ```
  </Step>

  <Step title="Step 4.7 - Add each signer to Yousign">
    *Make modules 14, 15, 16*

    Iterate over `signers` from step 4.6, one call per signer, in order:

    ```http theme={null}
    POST https://api-sandbox.yousign.app/v3/signature_requests/{signatureRequestId}/signers
    Authorization: Bearer <your yousign api key>
    Content-Type: application/json

    {
      "info": { "first_name": "Jane", "last_name": "Doe", "email": "jane.doe@acme.com", "locale": "en" },
      "signature_level": "electronic_signature",
      "signature_authentication_mode": "no_otp",
      "fields": [
        { "document_id": "<Yousign document id>", "type": "signature", "page": 1, "x": 241, "y": 227, "width": 85, "height": 37 }
      ]
    }
    ```
  </Step>

  <Step title="Step 4.8 - Activate the signature request">
    *Make module 17*

    Yousign sends the invitations to the signatories.

    ```http theme={null}
    POST https://api-sandbox.yousign.app/v3/signature_requests/{signatureRequestId}/activate
    Authorization: Bearer <your yousign api key>
    Content-Type: application/json

    {}
    ```
  </Step>
</Steps>

### Flow 2 - \[Yousign → Tomorro] Signature is completed

<Steps>
  <Step title="Step 0 - Receive the Yousign webhook">
    *Make module 1*

    Subscribe this webhook in Yousign to `signature_request.done`, and to the events of Flow 4.
  </Step>

  <Step title="Step 0.1 - Verify the Yousign signature">
    *Make modules 2, 3*

    The secret is read from the Make data store, key `yousign_webhook_secret`. Inputs: the raw body, the request headers and the secret.

    ```javascript theme={null}
    const crypto = require('crypto');
    const { rawBody, headers, secret } = input;

    const header = (headers || []).find((h) => String(h.name).toLowerCase() === 'x-yousign-signature-256');
    if (!header) return { valid: false, reason: 'missing X-Yousign-Signature-256 header' };

    const received = String(header.value).trim();
    const expected = `sha256=${crypto.createHmac('sha256', secret).update(rawBody).digest('hex')}`;
    const matches = expected.length === received.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
    if (!matches) return { valid: false, reason: 'signature mismatch' };

    return { valid: true, event: JSON.parse(rawBody) };
    ```
  </Step>

  <Step title="Step 0.2 - Route the event">
    *Make module 4*

    Continue only when `valid` is `true`. This flow takes `event_name` = `signature_request.done` with a `data.signature_request.external_id` (the Tomorro `contractId`). The other events go to Flow 4.
  </Step>

  <Step title="Step 0.3 - Download each signed document from Yousign">
    *Make modules 5, 6*

    Iterate over `data.signature_request.documents`, keep `nature` = `signable_document`:

    ```http theme={null}
    GET https://api-sandbox.yousign.app/v3/signature_requests/{signatureRequestId}/documents/{documentId}/download
    Authorization: Bearer <your yousign api key>
    ```
  </Step>

  <Step title="Step 1 - Get an upload URL, one per PDF">
    *Make module 7*

    ```http theme={null}
    POST https://api.tomorro.com/v2/files/upload-url
    x-api-key: <your api key>
    Content-Type: application/json

    { "contentType": "application/pdf" }
    ```
  </Step>

  <Step title="Step 2 - Upload the PDF to data.url">
    *Make module 8*

    No auth header, raw bytes of the PDF downloaded in step 0.3.

    ```http theme={null}
    PUT <data.url from step 1>
    Content-Type: application/pdf

    <raw bytes of the PDF>
    ```
  </Step>

  <Step title="Step 3 - Push the contract to Tomorro" />

  <Step title="Step 3.1 - Collect the uploaded files">
    *Make module 9*

    Aggregate one row per PDF, joined with a new line:

    ```text theme={null}
    <data.key from step 1>|<Yousign file name>
    ```
  </Step>

  <Step title="Step 3.2 - Build the signed-version body">
    *Make module 10*

    Inputs: the rows from step 3.1, `data.signature_request.completed_at` and `event_time` from the Yousign webhook.

    ```javascript theme={null}
    const { uploads, completedAt, eventTime } = input;

    const files = uploads.split('\n').filter(Boolean).map((row) => {
      const [key, ...rest] = row.split('|');
      const name = rest.join('|').trim() || 'Signed document.pdf';
      return { key, name: /\.pdf$/i.test(name) ? name : `${name}.pdf`, contentType: 'application/pdf' };
    });

    // event_time is a unix timestamp; Tomorro refuses a signatureDate in the future
    const toDate = (value) => new Date(/^\d+$/.test(String(value)) ? Number(value) * 1000 : value);
    const signedAt = toDate(completedAt || eventTime);
    const signatureDate = new Date(Math.min(signedAt.getTime(), Date.now())).toISOString();

    return { body: JSON.stringify({ files, signatureDate }) };
    ```
  </Step>

  <Step title="Step 3.3 - Push the signed version">
    *Make module 11*

    ```http theme={null}
    POST https://api.tomorro.com/v2/contracts/{external_id}/signed-version
    x-api-key: <your api key>
    Content-Type: application/json

    {
      "files": [
        { "key": "<data.key from step 1>", "name": "NDA - Acme.pdf", "contentType": "application/pdf" }
      ],
      "signatureDate": "2026-09-07T10:15:00.000Z"
    }
    ```
  </Step>

  <Step title="Step 3.4 - Accept 409 ContractAlreadySigned">
    *Make module 12*

    Inputs: status code and body of step 3.3 (the module runs with *Evaluate all states as errors* off).

    ```javascript theme={null}
    const { statusCode, body } = input;

    const status = Number(statusCode);
    if (status >= 200 && status < 300) return { outcome: 'signed' };

    const parsed = typeof body === 'string' ? (() => { try { return JSON.parse(body); } catch { return {}; } })() : body || {};
    // A previous delivery of the same Yousign webhook already pushed the signed version
    if (status === 409 && parsed.error?.errorId === 'ContractAlreadySigned') return { outcome: 'alreadySigned' };

    throw new Error(`signed-version failed with ${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
    ```
  </Step>
</Steps>

### Flow 3 - \[Tomorro → Yousign] Signature canceled in Tomorro

<Steps>
  <Step title="Step 0 - A user cancels the signature on Tomorro" />

  <Step title="Step 1 - Receive the signature.canceled webhook">
    *Make modules 1, 2, 3, 4*

    Same webhook, same verification and same router as Flow 1 step 1. This route takes `eventType` = `signature.canceled`.
  </Step>

  <Step title="Step 2 - Cancel in Yousign" />

  <Step title="Step 2.1 - Find the Yousign signature request">
    *Make module 18*

    Look it up by `external_id` = `contractId`, among the requests still running:

    ```http theme={null}
    GET https://api-sandbox.yousign.app/v3/signature_requests?external_id[eq]={contractId}&status[in]=ongoing,approval
    Authorization: Bearer <your yousign api key>
    ```
  </Step>

  <Step title="Step 2.2 - Build the Yousign cancel body">
    *Make module 19*

    Input: `data.cancelReason` from the webhook.

    ```javascript theme={null}
    const { cancelReason } = input;

    const body = { reason: 'contractualization_aborted' };
    if (cancelReason) body.custom_note = String(cancelReason).slice(0, 500);

    return { body: JSON.stringify(body) };
    ```
  </Step>

  <Step title="Step 2.3 - Cancel each matching request">
    *Make modules 20, 21*

    Iterate over `data` from step 2.1:

    ```http theme={null}
    POST https://api-sandbox.yousign.app/v3/signature_requests/{signatureRequestId}/cancel
    Authorization: Bearer <your yousign api key>
    Content-Type: application/json

    { "reason": "contractualization_aborted", "custom_note": "<cancelReason>" }
    ```

    Yousign then fires `signature_request.canceled`, which reaches Flow 4: its `cancel-signatures` call answers `400`, accepted as expected.
  </Step>
</Steps>

### Flow 4 - \[Yousign → Tomorro] Signature canceled in Yousign

<Steps>
  <Step title="Step 0 - Receive the Yousign webhook">
    *Make modules 1, 2, 3, 4*

    Same webhook, same verification and same router as Flow 2 step 0. This route takes `event_name` in `signature_request.declined`, `.rejected`, `.expired`, `.canceled`, `.deleted`, `.permanently_deleted`, with a `data.signature_request.external_id`.
  </Step>

  <Step title="Step 1 - Cancel the signature in Tomorro" />

  <Step title="Step 1.1 - Build the cancel-signatures body">
    *Make module 13*

    Input: `event_name` from the Yousign webhook.

    ```javascript theme={null}
    const { eventName } = input;

    const reasons = {
      'signature_request.declined': 'Refused by a signatory in Yousign',
      'signature_request.rejected': 'Rejected by an approver in Yousign',
      'signature_request.expired': 'Signature request expired in Yousign',
      'signature_request.canceled': 'Signature request canceled in Yousign',
      'signature_request.deleted': 'Signature request deleted in Yousign',
      'signature_request.permanently_deleted': 'Signature request deleted in Yousign',
    };

    return { body: JSON.stringify({ cancelReason: reasons[eventName] || 'Signature stopped in Yousign' }) };
    ```
  </Step>

  <Step title="Step 1.2 - Cancel the signature">
    *Make module 14*

    ```http theme={null}
    POST https://api.tomorro.com/v2/contracts/{external_id}/cancel-signatures
    x-api-key: <your api key>
    Content-Type: application/json

    { "cancelReason": "Refused by a signatory in Yousign" }
    ```
  </Step>

  <Step title="Step 1.3 - Accept 400">
    *Make module 15*

    Inputs: status code and body of step 1.2. A `400` means the contract is no longer in `signing` (echo of Flow 3, or already signed).

    ```javascript theme={null}
    const { statusCode, body } = input;

    const status = Number(statusCode);
    if (status >= 200 && status < 300) return { outcome: 'canceled' };
    // Contract no longer in signing: already canceled in Tomorro (echo of Flow 3, or a retry that went through) or signed
    if (status === 400) return { outcome: 'notInSigning' };

    throw new Error(`cancel-signatures failed with ${status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
    ```
  </Step>

  <Step title="Step 2 - Ignore the echo webhook">
    Tomorro fires `signature.canceled` back, which reaches Flow 3. Step 2.1 there only searches the `ongoing` and `approval` requests, finds none, and the flow stops.
  </Step>
</Steps>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.