> ## 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.

# Technical recipes

> Back up your signed contracts: prerequisites, the flows and the API reference for your IT team

Everything runs on one Tomorro webhook and two REST endpoints. How you write to the storage depends on your storage (SharePoint, Google Drive, Box, S3...).

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor U as Tomorro user
    participant T as Tomorro
    participant I as Integration<br/>customer middleware
    participant S as Storage<br/>SharePoint, Drive, S3...

    Note over U,S: Prerequisites: "automation" superadmin account, API key, contractSigned webhook created from that account, write access to the storage

    rect rgb(232, 244, 253)
    Note over U,S: Flow 0: Initialisation, run once by hand (Tomorro -> Storage)
    loop Each page of signed contracts
        I->>T: GET /v2/contracts?status=signed&limit=50
        T-->>I: contracts + pagination.next_cursor
        loop Each contract not in the storage yet
            I->>T: GET /v2/contracts/{id}/signed-files
            T-->>I: downloadUrl (valid 30 s)
            I->>S: Upload the signed PDF
        end
    end
    end

    rect rgb(232, 244, 253)
    Note over U,S: Flow 1: Contract signed (Tomorro -> Storage)
    U->>T: Last signatory signs
    T->>I: POST webhook contractSigned<br/>header Leeway-Signature, timeout 3 s
    I-->>T: 200 OK
    I->>T: GET /v2/contracts/{id}/signed-files
    T-->>I: downloadUrl (valid 30 s)
    opt Metadata or advanced folder tree
        I->>T: GET /v2/contracts/{id}
        T-->>I: contract type, counterparty, fields
        Note over I: Advanced: apply the Tomorro storage rules to find the folder
    end
    I->>S: Upload the signed PDF (+ metadata)
    end
```

## 1. Technical prerequisites

<Steps>
  <Step title="1.1 - Create Tomorro superadmin account">
    An existing admin creates a dedicated **superadmin** account from **Settings > Members** (e.g. `automation@customer.com`), not a personal account.

    * The API key and the webhook are bound to a **member**, not to the organization: they only see the contracts that member can see.
    * A **superadmin** sees **every contract** of the workspace, in every folder. Its webhook fires for every signed contract and its API key can download every signed file, without the account being added as a participant.
    * With an **admin** account, the webhook only fires for the contracts it takes part in (or that sit in no folder), and the backup misses the others. Use a superadmin.
    * Recommended: no personal mailbox behind it, and exclude it from Tomorro notification settings.
  </Step>

  <Step title="1.2 - API key">
    Generate the API key from that superadmin account. It does not expire. It is sent on every REST call in the `x-api-key` header: [Step 1: Get your API key](/quickstart)
  </Step>

  <Step title="1.3 - contractSigned webhook">
    **Logged in as the superadmin account**, create a webhook with the trigger `contractSigned` and the URL of your integration: [Webhooks](/webhooks/overview). Keep its signing secret to verify every delivery ([Technical documentation](/webhooks/technical-documentation)).
  </Step>

  <Step title="1.4 - Storage">
    Out of scope (depends on the storage). Create the destination folder (and the metadata columns, see [Non-Technical scoping](/guides/backup-your-signed-contracts/non-technical-scoping)), and give the integration write access to it.
  </Step>
</Steps>

## 2. Flows

### Flow 0 - \[Tomorro → Storage] Initialisation

<Steps>
  <Step title="Step 0 - Triggered by hand">
    Run once, when the integration goes live: the webhook of Flow 1 only fires for contracts signed **after** it is created. Re-running it is safe: contracts already in the storage are skipped.
  </Step>

  <Step title="Step 1 - List the signed contracts">
    ```http theme={null}
    GET https://api.tomorro.com/v2/contracts?status=signed&limit=50
    x-api-key: <your api key>
    ```

    Response `200`:

    ```json theme={null}
    {
      "data": [
        {
          "id": "7d12db6f-36a6-4dc5-b022-66471ff5xxxc",
          "name": "NDA - Acme",
          "status": "signed",
          "counterparty": { "id": "...", "name": "Acme" },
          "contractType": { "id": "...", "name": "NDA" },
          "contractUrl": "https://app.tomorro.com/acme/project/7d12db6f-36a6-4dc5-b022-66471ff5xxxc"
        }
      ],
      "pagination": { "limit": 50, "next_cursor": "eyJpZCI6...", "has_next": true, "has_previous": false }
    }
    ```

    <Accordion title="Notes">
      * Up to 50 contracts per page: while `pagination.has_next` is `true`, call again with `after=<pagination.next_cursor>`.
      * Only some contract types: add `contractTypeId=<id>` (one type per call).
      * Skip a contract whose file is already in the storage (file name contains the contract id, see step 3).
    </Accordion>
  </Step>

  <Step title="Steps 2 and 3 - Same as Flow 1">
    For each contract: get the signed files, upload them (Flow 1, steps 1 and 3).
  </Step>
</Steps>

### Flow 1 - \[Tomorro → Storage] Contract signed

**Simple backup (recommended)**: every signed contract lands in one folder.

<Steps>
  <Step title="Step 0 - Receive the contractSigned webhook">
    Verify the `Leeway-Signature` header, answer `2xx` **within 3 seconds**, then do the work asynchronously ([Technical documentation](/webhooks/technical-documentation)).

    ```json theme={null}
    {
      "eventId": "b21213e3-8a9a-4e04-9bfc-c4e53f123xxx",
      "webhookId": "2a76094c-1f2e-48c8-a47f-1add41234xxx",
      "createdAt": "2026-10-02T14:55:16.280Z",
      "eventType": "contractSigned",
      "data": {
        "contract": {
          "id": "7d12db6f-36a6-4dc5-b022-66471ff5xxxc",
          "name": "NDA - Acme",
          "status": "signed",
          "signatureDate": "2026-10-02T14:55:10.000Z",
          "typeId": "...",
          "externalCompany": { "id": "...", "name": "Acme" }
        },
        "signatories": [ { "id": "...", "name": "Jane Doe", "email": "jane.doe@acme.com" } ]
      }
    }
    ```

    <Accordion title="Notes">
      * Keep `data.contract.id`, and `name`, `signatureDate`, `typeId`, `externalCompany.name` for the file name and the metadata.
      * Only some contract types: skip the event when `data.contract.typeId` is not one of them.
      * **Deduplicate on `eventId`**: a delivery that fails is retried (up to 10 times, 5 minutes apart), so the same event can arrive more than once.
    </Accordion>
  </Step>

  <Step title="Step 1 - Get the signed files">
    ```http theme={null}
    GET https://api.tomorro.com/v2/contracts/{contractId}/signed-files
    x-api-key: <your api key>
    ```

    Response `200`:

    ```json theme={null}
    {
      "data": [
        {
          "id": "550e8400-...",
          "filename": "NDA - Acme - signed.pdf",
          "contentType": "application/pdf",
          "downloadUrl": "https://s3...",
          "expiresAt": "2026-10-02T14:55:46.000Z",
          "contractUrl": "https://app.tomorro.com/acme/project/7d12db6f-..."
        }
      ]
    }
    ```

    <Accordion title="Notes">
      * One entry per signed document: the main document, then the annexes signed in the same bundle, in order.
      * `downloadUrl` is a pre-signed URL valid **30 seconds**, see `expiresAt`. Download straight away (no auth header) and do not store the URL. Re-call the endpoint for a fresh one.
      * `409` when the contract has no signed file yet; `404` for an unknown contract.
    </Accordion>
  </Step>

  <Step title="Step 2 - Read the contract metadata (optional)">
    Only needed for metadata the webhook does not carry (contract type name, smart fields), or for the advanced folder tree below.

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

    Returns `contractType` (`id`, `name`), `counterparty`, `fields` (smart field values) and `contractUrl`.
  </Step>

  <Step title="Step 3 - Upload to the storage">
    Out of scope (depends on the storage). Upload each file to the destination folder.

    * **File name**: include the contract id (e.g. `NDA - Acme - 7d12db6f.pdf`): two contracts can share a name, and a re-run overwrites instead of duplicating.
    * **Metadata**: contract type, counterparty, signature date, `contractUrl`... as columns, if the storage supports them.
    * **Several files**: keep the order of step 1, e.g. `NDA - Acme - 7d12db6f.pdf`, then `NDA - Acme - 7d12db6f - Annex 1.pdf`.

    <Tip>
      **Storage unreachable**: retry with backoff (Make, n8n and Zapier have a retry-on-error setting), and log the failed uploads to a channel someone reads (email, Slack). Answering `2xx` to the webhook first means Tomorro does not retry for you.
    </Tip>
  </Step>
</Steps>

**Advanced: mirror the Tomorro folder tree.** Same flow, the folder is computed in step 2 instead of being fixed.

<Warning>
  Tomorro does **not** return the folder path of a contract. `GET /v2/contracts/{id}` gives a `folderId`, but no public endpoint turns it into a folder name or path. The folder comes from the **storage rules** of the contract type: the integration has to apply the same rules itself, and be updated every time a rule changes in Tomorro. Start with the simple backup unless you have a strong reason not to.
</Warning>

<Steps>
  <Step title="Step 2.1 - List your storage rules">
    Once, from the storage settings of each contract type in Tomorro ([Configure contract storage](https://help.tomorro.com/en/articles/13459997-configure-contract-storage)), write each rule as a condition on the contract's data and a target path. For example:

    | Contract type | Condition | Target folder |
    | - | - | - |
    | NDA | Counterparty country = France | `Legal/NDA/France` |
    | NDA | Any other | `Legal/NDA/International` |
    | Employment contract | - | `HR/Employment contracts/<signature year>` |
  </Step>

  <Step title="Step 2.2 - Compute the folder">
    Read the contract (step 2) and evaluate the rules of its `contractType.id` against `counterparty` and `fields` (smart field values) to get the target path. No rule matches: fall back to a single "To sort" folder rather than dropping the file.
  </Step>

  <Step title="Step 2.3 - Create the folder if needed">
    Out of scope (depends on the storage). Create each missing level of the path, then upload as in step 3.
  </Step>
</Steps>

## 3. Technical references

Reference material: conventions, the endpoints, the webhook and error handling. Read it once, then come back to it while implementing the flows above.

### 3.1 - API conventions

* **REST only.** The whole integration runs on the public REST API and one webhook.
* **Base URL**: `https://api.tomorro.com/v2`
* **Authentication**: `x-api-key: <your api key>` on every call. A missing or unknown key returns 401.
* **Versioning**: optional `tomorro-version` header, defaults to the latest version.
* **Rate limit**: a `429` means too many calls in a short time. Retry with backoff, especially during Flow 0.
* **Sandbox**: available on request. It is an empty sandbox, not a copy of your production workspace.
* **Response envelope**: every **successful** JSON response is wrapped in a `data` object (errors come as `{ error, meta }`, see 3.4).
* **Full reference**: [Tomorro API](/api-reference/introduction)

### 3.2 - Endpoints

| Endpoint | Use in this integration |
| - | - |
| `GET /v2/contracts?status=signed` | Flow 0: every signed contract. `limit` 1 to 50, cursor pagination (`after`), `contractTypeId` to narrow down. |
| `GET /v2/contracts/{id}/signed-files` | Flows 0 and 1: the signed PDFs, with a download URL valid 30 seconds. |
| `GET /v2/contracts/{id}` | Flow 1, optional: contract type, counterparty, smart field values, `folderId`. |

### 3.3 - The contractSigned webhook

* **When it fires**: when a contract becomes `signed`, whatever the signature tool: Tomorro signature, DocuSign, or a signed version pushed by a third-party signature tool.
* **When it does not**: contracts signed before the webhook was created, and contracts **imported directly as signed** ([bulk import](https://help.tomorro.com/en/articles/3727366-import-contracts-in-bulk), [store a signed document](https://help.tomorro.com/en/articles/10493914-store-a-signed-document)). Flow 0 covers both: re-run it after a bulk import.
* **Payload**: `data.contract` (`id`, `name`, `status`, `signatureDate`, `typeId`, `externalCompany`, `attributes`...) and `data.signatories`. No folder.
* **Delivery**: `2xx` within 3 seconds, otherwise retried up to 10 times, 5 minutes apart; after the 10th failure the webhook is **disabled** and has to be re-enabled in the settings. See [Delivery & retries](/webhooks/technical-documentation).
* **Security**: every delivery is signed (`Leeway-Signature` header). Verify it before calling the API.

### 3.4 - Error handling

Every endpoint follows the standard error format of the REST API.

**Error envelope**

```json theme={null}
{
  "error": {
    "statusCode": "409",
    "errorId": "optional, present on some domain errors",
    "message": "Human readable message"
  },
  "meta": {
    "timestamp": "2026-10-02T10:09:22.634Z",
    "requestId": "req_Ab12Cd34"
  }
}
```

* `error.statusCode` is a **string**, not a number.
* `meta.requestId` is the value to quote to Tomorro support when reporting a failing call. Log it.

| Code | Meaning | Retry? |
| - | - | - |
| `401` | Missing or unknown `x-api-key`. | No |
| `403` | The API key's account cannot see this contract: use a superadmin account (1.1). | No |
| `404` | Unknown contract. | No |
| `409` | `signed-files`: no signed file on this contract (not signed yet). | No |
| `429` | Rate limit exceeded. | Yes, with backoff |
| `500` / timeout | Internal error. | Yes, with backoff |

### 3.5 - Edge cases

* **Duplicates**: name each file with the contract id and deduplicate webhook deliveries on `eventId`: a retried event or a re-run of Flow 0 overwrites instead of adding a copy.
* **Webhook disabled** after 10 failed deliveries: events fired meanwhile are not replayed. Re-enable it, then re-run Flow 0 to catch up (already saved contracts are skipped).
* **Contract deleted or canceled in Tomorro after signature**: the backup keeps its copy. Nothing is removed from the storage.
* **Amendment**: a new contract in Tomorro, so a new file with its own id.
* **Unsigned contracts** (draft, negotiating): out of scope, their document is not a final PDF yet.
* **Signature certificate**: not part of `signed-files`. Download it from the contract in Tomorro if your backup needs it.


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