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

> Create contracts from your HRIS: prerequisites, the flows and the API reference for your IT team

Everything runs on the Tomorro REST API: one counterparty call, one contract call and one signature call per new employee. How you read and write the HRIS depends on your HRIS.

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor U as HR user
    participant H as HRIS<br/>employee records
    participant I as Integration<br/>customer middleware
    participant T as Tomorro
    actor E as Employee

    Note over U,E: Prerequisites: "automation" superadmin account, API key, contract type + template (smart fields, internal signatory, empty employee signature zone), "Tomorro contract" field in the HRIS

    rect rgb(232, 244, 253)
    Note over U,E: Flow 1: New employee in the HRIS (HRIS -> Tomorro -> HRIS)
    U->>H: Creates and validates the employee record
    H-->>I: New employee (event, webhook or polling)
    I->>T: POST /v2/counterparties<br/>name = first name + last name
    T-->>I: 201 data.id
    I->>T: POST /v2/contracts<br/>contractTypeId + templateId + counterparty.id<br/>fields + external signatory email
    T-->>I: 201 data.id + contractUrl
    I->>T: POST /v2/contracts/{id}/request-signatures
    T-->>I: 200 data.status = sent
    T->>E: Signature request email
    I->>H: Write the contract id and contractUrl into "Tomorro contract"
    end

    rect rgb(245, 229, 164)
    Note over U,E: Flow 2 (optional): Contract signed (Tomorro -> HRIS)
    E->>T: Signs
    Note over T: Internal signatory signs (order set on the template)
    T->>I: POST webhook contractSigned
    I-->>T: 200 OK
    I->>T: GET /v2/contracts/{id}/signed-files
    T-->>I: downloadUrl (valid 30 s)
    I->>H: Upload the signed PDF + signature date to the employee record
    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 is bound to a **member**, not to the organization. Every write performed through the API is attributed to that member and inherits its permissions.
    * Consequence: this account is the author of every employment contract created by the integration. A named account such as "HRIS automation" keeps the contract timeline readable.
    * 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 - Contract type and template in Tomorro">
    The template carries everything the integration does not send.

    * **Contract type** (e.g. *Employment contract*): its **default signature tool is Tomorro**, so the employee receives the signature email from Tomorro.
    * **Smart fields**: one per mapped HRIS field (**Libraries > Dynamic components > Smart fields > New**), placed in the template. Pick the type matching the HRIS: *Text* for names, *Date* for the date of birth, *Number* for the salary, *Single-choice selector* for the job title and the grade (options = the HRIS values, see 3.3).
    * **Signatories on the template**: the **internal signatory** is set on the template (e.g. the HR Director), and the **external signatory** has a signature zone but no one assigned. The integration fills that empty slot with the employee's email.
    * **One template per job** if the contract differs by job: keep a `job title → templateId` table in the integration.
    * **One template per language**: the contract takes the **language of its template** (`language` in `GET /v2/contract-types/{id}/templates`, e.g. `FR`, `EN`). To generate a contract in English, create the English template and pick it: the table becomes `job title + language → templateId`.
    * Copy the ids next to your mapping ([Non-Technical scoping](/guides/create-contracts-from-your-hris/non-technical-scoping)): the contract type id from `GET /v2/contract-types`, the template ids from `GET /v2/contract-types/{id}/templates`, the smart field ids from `GET /v2/contract-types/{id}/creation-form` (or the last segment of the smart field URL).

    | Mapping | API name |
    | - | - |
    | Contract type | `contractTypeId` |
    | Template (per job and language) | `templateId` |
    | Last name | `3f2b8c1e-...` (smart field id) |
    | First name | `8a41d0f7-...` |
    | Date of birth | `c9e07b52-...` |
    | Email | `signatories.externalSignatoryEmailList` |
    | Salary | `51d6a3e9-...` |
    | Job title | `e7b94f20-...` (option label, see 3.3) |
    | Grade | `0c5f8d16-...` (option label, see 3.3) |
    | Tomorro contract | `id` and `contractUrl` (response) |
  </Step>

  <Step title="1.4 - Approval before signature">
    If an approval workflow runs **before signature** on this contract type, the contract goes to approval first and is not sent to the employee straight away (Flow 1, step 3). Decide with the business owner: no approval on this contract type, or keep it and let the contract wait for it.
  </Step>

  <Step title="1.5 - HRIS: add the Tomorro contract field">
    Add a **text** field `Tomorro contract` to the employee record, empty for every employee. The integration fills it with the contract id (and `contractUrl` if there is room); users must not edit it. It stops a second contract from being created for the same employee.
  </Step>
</Steps>

## 2. Flows

### Flow 1 - \[HRIS → Tomorro → HRIS] New employee in the HRIS

<Steps>
  <Step title="Step 0 - Triggered by the HRIS">
    Out of scope. An employee record is created and validated in the HRIS: HRIS event or webhook if it has one, otherwise poll the employees created since the last run. Skip it if `Tomorro contract` is already filled, or if a mapped field or the email is empty.
  </Step>

  <Step title="Step 1 - Create the employee as a counterparty">
    ```http theme={null}
    POST https://api.tomorro.com/v2/counterparties
    x-api-key: <your api key>
    Content-Type: application/json

    { "name": "Jane Doe" }
    ```

    Response `201`:

    ```json theme={null}
    {
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Jane Doe",
        "status": "active"
      }
    }
    ```

    <Accordion title="Notes">
      * Keep `data.id` for step 2.
      * **Why a separate call**: `POST /v2/contracts` also accepts `counterparty.name`, but it then **reuses** any counterparty with the same name (case and spaces ignored). Two employees named Jane Doe would share one counterparty. Creating it here, then passing its `id`, gives every employee their own.
    </Accordion>
  </Step>

  <Step title="Step 2 - Create the employment contract">
    ```http theme={null}
    POST https://api.tomorro.com/v2/contracts
    x-api-key: <your api key>
    Content-Type: application/json

    {
      "contractTypeId": "6f1c2d3e-...",
      "templateId": "9a8b7c6d-...",
      "name": "Employment contract - Jane Doe",
      "counterparty": { "id": "550e8400-e29b-41d4-a716-446655440000" },
      "fields": [
        { "fieldId": "3f2b8c1e-...", "value": "Doe" },
        { "fieldId": "8a41d0f7-...", "value": "Jane" },
        { "fieldId": "c9e07b52-...", "value": "1994-03-12" },
        { "fieldId": "51d6a3e9-...", "value": "48000" },
        { "fieldId": "e7b94f20-...", "value": "Account Executive" },
        { "fieldId": "0c5f8d16-...", "value": "Level 2" }
      ],
      "signatories": {
        "externalSignatoryEmailList": ["jane.doe@example.com"]
      }
    }
    ```

    Response `201`:

    ```json theme={null}
    {
      "data": {
        "id": "7d12db6f-36a6-4dc5-b022-66471ff5xxxc",
        "name": "Employment contract - Jane Doe",
        "status": "draft",
        "templateId": "9a8b7c6d-...",
        "signatories": [ "... the internal signatory of the template, and jane.doe@example.com ..." ],
        "contractUrl": "https://app.tomorro.com/acme/project/7d12db6f-36a6-4dc5-b022-66471ff5xxxc"
      }
    }
    ```

    <Accordion title="Notes">
      * **`templateId` is required in practice**: without it, the contract has no document and cannot be sent for signature. It also sets the contract **language**.
      * **`fields`**: one entry per smart field, `value` always a **string**. Dates as `YYYY-MM-DD`, numbers without spaces or currency (`"48000"`), picklists as the **exact option label** (see 3.3). Leave a field out rather than sending an empty string.
      * **Signatories**: only send the employee's email. It fills the **empty external signature zone** of the template; the internal signatory is already on the template. Do not add more signatories than the template has empty zones: the extra ones get no signature zone and step 3 is refused.
      * Required fields of the contract type's creation form that are missing: `400 IncompleteCreationForm`. Unknown `contractTypeId` or `templateId`: `404`.
      * Keep `data.id` and `data.contractUrl`.
    </Accordion>
  </Step>

  <Step title="Step 3 - Send the contract for signature">
    ```http theme={null}
    POST https://api.tomorro.com/v2/contracts/{contractId}/request-signatures
    x-api-key: <your api key>
    Content-Type: application/json

    {
      "signatureNote": "Welcome aboard! Here is your employment contract.",
      "isReminderEnabled": true
    }
    ```

    Response `200`:

    ```json theme={null}
    {
      "data": {
        "id": "a1b2c3d4-...",
        "status": "sent",
        "signatories": [ "..." ],
        "sentAt": "2026-10-02T09:15:00.000Z",
        "contractUrl": "https://app.tomorro.com/acme/project/7d12db6f-36a6-4dc5-b022-66471ff5xxxc"
      }
    }
    ```

    <Accordion title="Notes">
      * Can be called **right after step 2**: the document and its signatories are ready when step 2 answers.
      * `documentId` is optional: without it, the last document of the contract is sent.
      * **Always check `data.status`**:
        * `sent`: done. The contract moves to `signing`, Tomorro emails the signatories in the signing order of the contract type and template.
        * `draft`: the signature was **not** sent. Usually an approval workflow before signature (1.4): the contract waits for it. Alert HR with `contractUrl`.
      * `signatureNote` is the message shown to the signatories. `isReminderEnabled` defaults to `true`.

      **Errors on step 3** (`400`, branch on `error.errorId`)

      | `errorId` | Meaning | What to do |
      | - | - | - |
      | `StartSignatureBlankSignatory` | A signature zone of the template has no one assigned (email missing in step 2, or a second empty zone). | Fix the template or the email, then retry. |
      | `StartSignatureMissingFields` | A signatory has no signature zone: more signatories sent in step 2 than empty zones on the template. | Fix the template, recreate the contract. |
      | `StartSignatureMissingSignatory` | No signatory at all on the contract. | Check the template signatories (1.3). |
      | `StartSignatureMissingDocumentSmartFieldsForParty` | A smart field of the document is still empty. | Send it in step 2, or fill it from `contractUrl`. |
      | `ContractNotReadyToPrepareSignature` | The contract cannot be sent as is: approval pending, pending suggestions, document already sent... | Open `contractUrl` and finish by hand. |
    </Accordion>
  </Step>

  <Step title="Step 4 - Write the contract back to the HRIS">
    Out of scope (depends on the HRIS). Write `data.id` (and `contractUrl`) from step 2 into `Tomorro contract`, straight away: until it is written, a new run would create a second contract for the same employee.

    <Tip>
      **Rate limit**: no need to slow the calls down. If Tomorro answers `429`, retry with backoff.
    </Tip>
  </Step>
</Steps>

### Flow 2 (optional) - \[Tomorro → HRIS] Contract signed

<Steps>
  <Step title="Step 0 - Receive the contractSigned webhook">
    Create the webhook **from the automation account** (1.1), trigger `contractSigned`: [Webhooks](/webhooks/overview). Webhooks only fire for contracts that concern the member who created them, and this account is the author of every contract of Flow 1.

    Verify the `Leeway-Signature` header ([Technical documentation](/webhooks/technical-documentation)), answer `2xx` straight away, then keep `data.contract.id`, and `data.contract.signatureDate` for the HRIS.
  </Step>

  <Step title="Step 1 - Find the employee">
    Out of scope (depends on the HRIS). Look up the employee whose `Tomorro contract` field holds `data.contract.id` (Flow 1, step 4). No employee: the contract was not created by Flow 1, ignore the event.
  </Step>

  <Step title="Step 2 - Get the signed PDF">
    ```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": "Employment contract - Jane Doe - signed.pdf",
          "contentType": "application/pdf",
          "downloadUrl": "https://s3...",
          "expiresAt": "2026-10-03T10:45:30.000Z",
          "contractUrl": "https://app.tomorro.com/acme/project/7d12db6f-..."
        }
      ]
    }
    ```

    `downloadUrl` is valid **30 seconds**: download the file straight away (no auth header), and re-call the endpoint for a fresh URL if needed.
  </Step>

  <Step title="Step 3 - Upload to the employee record">
    Out of scope (depends on the HRIS). Upload the PDF to the employee's documents and write the signature date. With Lucca, for instance, attach the uploaded file to the work contract by its `fileId`.
  </Step>
</Steps>

## 3. Technical references

Reference material: conventions, the endpoints, field values 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.
* **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.
* **Sandbox**: available on request. It is an empty sandbox, not a copy of your production workspace.
* **Content type**: `application/json`
* **Response envelope**: every **successful** JSON response is wrapped in a `data` object (errors come as `{ error, meta }`, see 3.4). Read `data.id`, `data.status` and so on.
* **Full reference**: [Tomorro API](/api-reference/introduction)

### 3.2 - Endpoints

| Endpoint | Use in this integration |
| - | - |
| `GET /v2/contract-types` | Setup: the contract type id. |
| `GET /v2/contract-types/{id}/templates` | Setup: the template ids, their `language` and their smart fields. |
| `GET /v2/contract-types/{id}/creation-form` | Setup: the smart fields of the creation form, and which are `required`. |
| `POST /v2/counterparties` | Flow 1, step 1: the employee as a counterparty. Returns `201`. |
| `POST /v2/contracts` | Flow 1, step 2: the contract, its document, its fields and the employee as signatory. Returns `201`. |
| `POST /v2/contracts/{id}/request-signatures` | Flow 1, step 3: send for signature. Returns `200` with `data.status`. |
| `GET /v2/contracts?counterpartyId={id}` | Recovery: the contracts of an employee, to check a creation went through (3.4). |
| `GET /v2/contracts/{id}/signed-files` | Flow 2: the signed PDF. |

### 3.3 - Field values

`fields[].value` is always a string. Tomorro does not reformat it: send it in the format below.

| Smart field type | Send | Example |
| - | - | - |
| Text | The text | `"Doe"` |
| Date | `YYYY-MM-DD` | `"1994-03-12"` |
| Number | Digits, `.` for decimals, no spaces or currency | `"48000"` |
| Single-choice selector | The **exact option label**, case included | `"Account Executive"` |
| Multi-choice selector | Exact labels separated by `;` | `"French;English"` |

* **Picklists**: a label that matches no option is not refused, it is stored as an unknown value and shows empty in Tomorro. Align the labels on both sides first (job titles, grades), and translate HRIS codes into Tomorro labels in the integration when they differ.
* **Built-in contract fields** can be set the same way, by name instead of id: `startAt` (start date, `YYYY-MM-DD`), `endAt` (end date, for a fixed-term contract).
* `null` clears a field.

### 3.4 - Error handling

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

**Error envelope**

```json theme={null}
{
  "error": {
    "statusCode": "400",
    "errorId": "optional, present on some domain errors",
    "message": "Human readable message",
    "details": [
      { "field": "fields.0.value", "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? |
| - | - | - |
| `400` | Invalid body (unknown key, wrong type), `IncompleteCreationForm` on contract creation, or a signature blocker on `request-signatures` (see Flow 1, step 3). | No, fix the payload or the template |
| `401` | Missing or unknown `x-api-key`. | No |
| `403` | The API key's account is not allowed to create contracts of this type or to manage counterparties. | No, check 1.1 |
| `404` | Unknown contract type, template or contract. | No |
| `429` | Rate limit exceeded. | Yes, with backoff |
| `500` / timeout | Internal error. | Yes, with backoff. On a contract creation, check first that it was not created (`GET /v2/contracts?counterpartyId=` the id of step 1) to avoid a duplicate. |

### 3.5 - Edge cases

* **Link**: the `Tomorro contract` field in the HRIS. Never match on the employee's name.
* **Employee created twice in the HRIS, or flow re-run**: skipped as long as `Tomorro contract` is filled (Flow 1, step 0).
* **Rehire**: a returning employee already has a counterparty. Reuse its id (store it in the HRIS too) instead of creating a second one in step 1.
* **Missing email**: no external signatory, the contract cannot go to signature. Skip the employee in step 0 and alert HR.
* **Data changed in the HRIS after the contract was sent**: the integration does not update a contract in signature. Cancel it in Tomorro and clear `Tomorro contract` to generate a new one.
* **Triggers**: prefer an HRIS webhook or event on "employee validated", so Flow 1 runs as soon as the record is ready. Without one, poll the employees created since the last run, e.g. every 15 minutes.


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