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

> Sync your counterparties: prerequisites, the four flows and the API reference for your IT team

Everything runs on the Tomorro REST API: three counterparty endpoints and one picklist endpoint. How you read and write the ERP depends on your ERP.

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor U as ERP user
    participant E as ERP<br/>counterparty base
    participant I as Integration<br/>customer middleware
    participant T as Tomorro<br/>counterparties

    Note over U,T: Prerequisites: "automation" superadmin account, API key, counterparty smart fields, "ID tomorro" column in the ERP

    rect rgb(232, 244, 253)
    Note over U,T: Flow 0: Initialisation, run once by hand (ERP -> Tomorro -> ERP)
    I->>T: GET /v2/smart-fields/{id}/options (picklist option ids)
    T-->>I: options
    I->>E: List counterparties with an empty ID tomorro
    E-->>I: counterparties
    loop Each counterparty
        I->>T: POST /v2/counterparties<br/>name + fields (smart field id -> value)
        T-->>I: 201 data.id
        I->>E: Write data.id into ID tomorro
    end
    end

    rect rgb(232, 244, 253)
    Note over U,T: Flow 1: Counterparty created in the ERP (ERP -> Tomorro -> ERP)
    U->>E: Creates a counterparty
    E-->>I: New counterparty (event or polling)
    I->>T: POST /v2/counterparties
    T-->>I: 201 data.id
    I->>E: Write data.id into ID tomorro
    end

    rect rgb(243, 229, 245)
    Note over U,T: Flow 2: Counterparty updated in the ERP (ERP -> Tomorro)
    U->>E: Edits a counterparty
    E-->>I: Updated counterparty (event or polling)
    I->>T: GET /v2/counterparties/{ID tomorro}
    T-->>I: current name + fields
    opt Something differs
        I->>T: PATCH /v2/counterparties/{ID tomorro}<br/>only the changed values
        T-->>I: 200
    end
    end

    rect rgb(245, 229, 164)
    Note over U,T: Flow 3: Counterparty updated in Tomorro, daily at 8:00 (Tomorro -> ERP)
    I->>T: GET /v2/counterparties?sort=-updatedAt
    T-->>I: counterparties, latest updates first
    Note over I: Keep those updated since the last run
    loop Each updated counterparty
        I->>E: Find the ERP row by ID tomorro
        opt Something differs
            I->>E: Update the ERP row with the Tomorro values
        end
    end
    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: the counterparty history shows this account as the author of every creation and update made by the integration. A named account such as "ERP automation" keeps it 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 - Counterparty smart fields in Tomorro">
    Every ERP field other than the name needs a smart field on the counterparty.

    * **Libraries > Dynamic components > Smart fields > New**, with **Data related to** = *Counterparty smart field*.
    * Pick the type matching the ERP: *Text* for SIRET, address and city, *Single-choice selector* for the industry (options = the ERP values, see 3.5).
    * Copy each smart field id from the URL of the field (last segment) next to your mapping ([Non-Technical scoping](/guides/sync-your-counterparties/non-technical-scoping)):

    | Mapping | API name |
    | - | - |
    | Counterparty name | `name` |
    | SIRET | `9d6175af-86a5-497a-aa42-facfb2b8dafb` |
    | Industry | `b25c969a-2a1c-4903-be86-10269aeb5e84` (option id, see 3.5) |
    | Address | `0abf8d86-02ab-4583-aea3-b730ad802c37` |
    | City | `a6690cab-baaa-4848-8866-a03d800049e3` |
    | ID tomorro | `id` (response) |
  </Step>

  <Step title="1.4 - ERP: add the ID tomorro column">
    Add a **text** column `ID tomorro` to the counterparty table, empty for every counterparty. The integration fills it; users must not edit it. If the ERP supports it, make it read-only for users.
  </Step>
</Steps>

## 2. Flows

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

<Steps>
  <Step title="Step 0 - Triggered by hand">
    Run once, when the integration goes live. Take every counterparty whose `ID tomorro` is empty (and that matches your scope, see [Non-Technical scoping](/guides/sync-your-counterparties/non-technical-scoping)). Re-running it is safe: linked counterparties are skipped.
  </Step>

  <Step title="Step 1 - Get the picklist option ids">
    Once per run, for each picklist smart field:

    ```http theme={null}
    GET https://api.tomorro.com/v2/smart-fields/b25c969a-2a1c-4903-be86-10269aeb5e84/options?limit=50
    x-api-key: <your api key>
    ```

    Response `200`:

    ```json theme={null}
    {
      "data": [
        { "id": "7c1e2f0a-...", "translations": [ { "language": "gb", "value": "Logistics" } ], "integrationMappings": [] },
        { "id": "0d93b6c4-...", "translations": [ { "language": "gb", "value": "Energy" } ], "integrationMappings": [] }
      ],
      "pagination": { "limit": 50, "next_cursor": null, "has_next": false, "has_previous": false }
    }
    ```

    <Accordion title="Notes">
      * Build a `label → id` table from `data[].translations[].value` and `data[].id`. Match labels case-insensitively.
      * Up to 50 options per page: follow `pagination.next_cursor` with `after` if there are more.
      * `integrationMappings` can hold your ERP codes for each option, to translate on codes instead of labels.
    </Accordion>
  </Step>

  <Step title="Step 2 - Create the counterparty in Tomorro, one call per ERP row">
    ```http theme={null}
    POST https://api.tomorro.com/v2/counterparties
    x-api-key: <your api key>
    Content-Type: application/json

    {
      "name": "Acme Logistics",
      "fields": {
        "9d6175af-86a5-497a-aa42-facfb2b8dafb": "41234567800012",
        "b25c969a-2a1c-4903-be86-10269aeb5e84": "7c1e2f0a-...",
        "0abf8d86-02ab-4583-aea3-b730ad802c37": "12 rue de la Paix",
        "a6690cab-baaa-4848-8866-a03d800049e3": "Paris"
      }
    }
    ```

    Response `201`:

    ```json theme={null}
    {
      "data": {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Acme Logistics",
        "status": "active",
        "logoUrl": null,
        "fields": {
          "b25c969a-2a1c-4903-be86-10269aeb5e84": { "type": "select", "value": "7c1e2f0a-..." },
          "a6690cab-baaa-4848-8866-a03d800049e3": { "type": "string", "value": "Paris" }
        },
        "createdAt": "2026-09-30T08:00:00.000Z",
        "updatedAt": "2026-09-30T08:00:00.000Z"
      }
    }
    ```

    <Accordion title="Notes">
      * `name` is the only required field. `status` defaults to `active`.
      * `fields` keys are **smart field ids** (1.3). Text values as strings, picklists as the **option id** (step 1, 3.5). Leave a field out rather than sending an empty string.
      * The body is strict: an unknown top-level key is refused with a `400`. An unknown smart field id is refused too.
      * Read the new id under `data.id`. In the response, each field comes back as `{ type, value }`, one entry per counterparty smart field of the workspace, filled or not.
      * **Duplicates**: Tomorro does not deduplicate counterparties. Calling this endpoint twice creates two counterparties. This is why Flows 0 and 1 only take counterparties with an empty `ID tomorro`.
    </Accordion>
  </Step>

  <Step title="Step 3 - Write the Tomorro id back to the ERP">
    Out of scope (depends on the ERP). Write `data.id` from step 2 into the `ID tomorro` column, straight away: until it is written, a new run would create the counterparty a second time.

    <Tip>
      **Rate limit**: no need to slow the calls down. If Tomorro answers `429`, retry with backoff: the Make example retries automatically, 3 times, 1 minute apart.
    </Tip>
  </Step>
</Steps>

### Flow 1 - \[ERP → Tomorro → ERP] Counterparty created in the ERP

<Steps>
  <Step title="Step 0 - Triggered by the ERP">
    Out of scope. A counterparty is created in the ERP: ERP event or webhook if it has one, otherwise poll the counterparties created since the last run. Skip it if its `ID tomorro` is already filled or its name is empty.
  </Step>

  <Step title="Steps 1 to 3 - Same as Flow 0">
    Get the option ids, create the counterparty, write `data.id` back to the ERP.
  </Step>
</Steps>

### Flow 2 - \[ERP → Tomorro] Counterparty updated in the ERP

<Steps>
  <Step title="Step 0 - Triggered by the ERP">
    Out of scope. A counterparty is updated in the ERP. Only counterparties with an `ID tomorro` go further.
  </Step>

  <Step title="Step 1 - Read the current counterparty and the option ids">
    ```http theme={null}
    GET https://api.tomorro.com/v2/counterparties/{ID tomorro}
    x-api-key: <your api key>
    ```

    Response `200`: same shape as the creation response. Read `data.name` and `data.fields["<smart field id>"].value`. Get the option ids as in Flow 0 step 1.
  </Step>

  <Step title="Step 2 - Compare, then update only what changed">
    Compare every mapped ERP value with the Tomorro value (picklists on the option id). If nothing differs, **stop**: the ERP change was on another column, or it is the echo of Flow 1 or Flow 3 (3.3). Otherwise:

    ```http theme={null}
    PATCH https://api.tomorro.com/v2/counterparties/{ID tomorro}
    x-api-key: <your api key>
    Content-Type: application/json

    {
      "fields": {
        "b25c969a-2a1c-4903-be86-10269aeb5e84": "0d93b6c4-...",
        "a6690cab-baaa-4848-8866-a03d800049e3": null
      }
    }
    ```

    Response `200`: the updated counterparty.

    <Accordion title="Notes">
      * Only the keys you send change. Send `name` only if it changed, and only the smart fields that changed.
      * `null` **clears** a smart field (the value was emptied in the ERP).
      * `404`: the counterparty no longer exists in Tomorro (deleted, or wrong `ID tomorro`). Apply the rule chosen in 3.6.
      * To archive a counterparty deactivated in the ERP, send `{ "status": "archived" }`; `{ "status": "active" }` restores it.
    </Accordion>
  </Step>
</Steps>

### Flow 3 - \[Tomorro → ERP] Counterparty updated in Tomorro

<Steps>
  <Step title="Step 0 - Scheduled, every day at 8:00">
    Read the last `updatedAt` you processed, saved at the end of the previous run. On the first run, look back 24 hours.
  </Step>

  <Step title="Step 1 - List the counterparties updated since the last run">
    One call returns the latest updates first:

    ```http theme={null}
    GET https://api.tomorro.com/v2/counterparties?sort=-updatedAt&limit=50
    x-api-key: <your api key>
    ```

    Keep the counterparties whose `updatedAt` is after the saved value, then save the newest `updatedAt` for the next run. Get the option ids once (Flow 0 step 1).

    <Accordion title="Notes">
      * `updatedAt` moves on every change of the counterparty, smart fields included.
      * If all 50 counterparties of the page changed since the last run, more are waiting: follow `pagination.next_cursor` with `after`.
      * Counterparties changed by Flow 2 come back here too: the comparison of step 2 finds nothing to write (3.3).
    </Accordion>
  </Step>

  <Step title="Step 2 - Update the ERP with the values that differ">
    Out of scope (depends on the ERP). For each Tomorro counterparty, find the ERP row whose `ID tomorro` equals that counterparty's `id`. No row: the counterparty exists only in Tomorro, ignore it (see [Non-Technical scoping](/guides/sync-your-counterparties/non-technical-scoping)). Otherwise translate picklist option ids back to labels, compare with the ERP and write only the differences. No difference, no write: an unnecessary write would fire Flow 2 again.
  </Step>
</Steps>

## 3. Technical references

Reference material: conventions, the counterparty object 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.fields` and so on.
* **Full reference**: [Counterparties - Tomorro API](/api-reference/introduction)

### 3.2 - Counterparty endpoints and fields

| Endpoint | Use in this integration |
| - | - |
| `POST /v2/counterparties` | Flows 0, 1: create. Returns `201` with the counterparty. |
| `GET /v2/counterparties/{id}` | Flows 2, 3: read the current values. |
| `PATCH /v2/counterparties/{id}` | Flow 2: update. Only the keys sent change. Returns `200`. |
| `GET /v2/smart-fields/{id}/options` | All flows: picklist option ids and labels. `limit` 1 to 50, cursor pagination, `term` filters on the label. |
| `GET /v2/counterparties` | Flow 3: `sort=-updatedAt` returns the latest updates first. `limit` 1 to 50, cursor pagination, `name` (partial match), `status`. Also useful to spot duplicates before Flow 0. |

| Field | Values | Note |
| - | - | - |
| `name` | string, required on creation | The counterparty name shown everywhere in Tomorro. |
| `status` | `active` , `archived` | Default `active`. Archive rather than delete. |
| `fields` (request) | `{ "<smart field id>": value }` | Text as a string, picklist as the option id, multi-choice as an array of option ids, `null` to clear. |
| `fields` (response) | `{ "<smart field id>": { type, value } }` | One entry per counterparty smart field of the workspace. `value` is `null` when empty, an option id for a picklist. |
| `fields[].type` | `string` , `number` , `date` , `select` , `multiselect` , `duration` , `rich_text` , `file` | Type of the smart field. |
| `updatedAt` | ISO 8601 | Moves on every change of the counterparty, smart fields included. Flow 3 relies on it. |

### 3.3 - Loops and conflicts

Flows 1 and 3 write into the ERP (so Flow 2 sees an updated counterparty), Flow 2 writes into Tomorro (so Flow 3 reads a different counterparty). Without care, a change bounces forever.

* **Compare before writing, in both directions.** Flow 2 only sends a `PATCH` when an ERP value differs from Tomorro; Flow 3 only writes the ERP when a Tomorro value differs. The echo of a write finds identical values and stops.
* **Compare normalised values**: trim spaces, treat empty and `null` the same, compare picklists on the option id.
* **Same-day conflict**: if a field is changed in both systems between two runs of Flow 3, the ERP value wins, since Flow 2 runs first.

### 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": "name", "message": "..." }
    ]
  },
  "meta": {
    "timestamp": "2026-09-30T10: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: missing `name`, unknown key, wrong value type. | No, fix the payload |
| `401` | Missing or unknown `x-api-key`. | No |
| `403` | The API key's account is not allowed to manage counterparties. | No, check 1.1 |
| `404` | Unknown counterparty id, or unknown smart field id in `fields`. | No, see 3.6 |
| `429` | Rate limit exceeded. | Yes, with backoff |
| `500` / timeout | Internal error. | Yes, with backoff. On a creation, check first that the counterparty was not created (`GET /v2/counterparties?name=...`) to avoid a duplicate. |

### 3.5 - Picklists

* A picklist value is stored as the **option id**, not its label. Send `"Logistics"` and the API accepts it, but Tomorro shows an empty field.
* Get the ids with `GET /v2/smart-fields/b25c969a-2a1c-4903-be86-10269aeb5e84/options` and translate label ↔ id in the integration, in both directions.
* Align the labels on both sides first (here, `Software & Saas` in Tomorro renamed `Software & SaaS`). A label missing from Tomorro is left empty, never invented.

### 3.6 - Edge cases

* **Link**: an `ID tomorro` column in the ERP. Never match on name or SIRET.
* **Existing counterparties** before Flow 0: clean them up, or link them by filling `ID tomorro` by hand (Flow 0 skips linked rows).
* **Counterparty deactivated in the ERP**: archive the counterparty (`status: archived`) or ignore.
* **Counterparty deleted in Tomorro**: Flows 2 and 3 get a `404`. Alert, or clear `ID tomorro`.
* **Loops**: every update flow compares before writing, so the echo of a write stops by itself (3.3).
* **Triggers**: prefer an ERP webhook or event, so Flows 1 and 2 run as soon as a counterparty changes. Without one, poll the counterparties changed since the last run, e.g. every 15 minutes.


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