[Intum Dev](https://intum.dev/index.md) / [Oprogramowanie dla firm](https://intum.dev/oprogramowanie-dla-firm.md)

# [Identyfikatory z zewnętrznych systemów: jak rozwiązują to Salesforce, HubSpot, Zendesk i Intum](https://intum.dev/oprogramowanie-dla-firm/identyfikatory-z-zewnetrznych-systemow-jak-rozwiazuja-to-salesforce-hubspot-zendesk-i-intum.md) | [API](#api)

Każda integracja z systemem klienta zaczyna się od tego samego pytania: skąd wiadomo, że firma przysłana dziś to ta sama, która przyszła tydzień temu. Bez odpowiedzi każda synchronizacja tworzy duplikaty. Producenci oprogramowania rozwiązali to na kilka sposobów i warto je porównać, bo różnice widać dopiero wtedy, gdy do CRM zaczynają płynąć dane z drugiego i trzeciego systemu.

## Jedno pole na identyfikator

Zendesk i Intercom dają jedno pole `external_id` obok swojego wewnętrznego numeru. Wpisujesz w nie identyfikator z własnego systemu i po nim odnajdujesz rekord.

Działa, dopóki jest jedno źródło. Gdy dane zaczynają płynąć jednocześnie z ERP i ze sklepu, pole jest już zajęte i drugie źródło musi sobie radzić inaczej: zwykle przez dodatkowe pole własne albo tabelę mapowań utrzymywaną po stronie integratora.

## Wiele pól identyfikacyjnych

Salesforce poszedł dalej. Dowolne pole można oznaczyć jako External ID, a potem aktualizować rekord bezpośrednio po jego wartości. Można mieć osobne pole dla każdego systemu źródłowego, więc ERP i sklep sobie nie przeszkadzają.

HubSpot działa podobnie: własna właściwość oznaczona jako unikalna, a w API wskazuje się, po której właściwości ma nastąpić dopasowanie.

Cena tej elastyczności to konfiguracja. Zanim ruszy pierwsza synchronizacja, ktoś musi wejść w ustawienia i założyć pole na identyfikator, przypilnować unikalności i pamiętać, które pole należy do którego systemu.

## Identyfikator schowany w metadanych

Stripe nie ma pojęcia identyfikatora zewnętrznego jako osobnej rzeczy. Jest za to `metadata` - dowolne pary klucz-wartość na obiekcie, gdzie wkłada się własny numer. Do ochrony przed podwójnym przetworzeniem tego samego żądania służy osobny mechanizm, klucz idempotencji przesyłany w nagłówku.

To bardzo elastyczne i zupełnie nieuporządkowane. Metadane nie są unikalne, więc nic nie zatrzyma dwóch obiektów z tym samym numerem, a odnajdywanie po nich bywa wolniejsze niż po zwykłym polu.

## Brak wsparcia i tabela mapowań u klienta

Część systemów, jak Pipedrive, nie ma dedykowanego rozwiązania. Integratorzy zakładają wtedy pole własne albo trzymają mapowanie „mój numer to ich numer" u siebie, w osobnej tabeli.

Efekt jest przewidywalny: wiedza o tym, co jest czym, wyprowadza się z CRM do skryptu integracyjnego. Gdy skrypt zmienia właściciela albo przestaje działać, nikt już nie wie, skąd wziął się dany klient.

## Jak to działa w Intum

Rozdzieliliśmy dwie rzeczy, które w innych systemach zwykle siedzą w jednym polu, bo służą do czegoś innego.

**Identyfikator ze źródła** przychodzi w polu `id` i ląduje w osobnej przestrzeni nazwanej kodem systemu. Każdy system dostaje swoją przestrzeń przy pierwszej wysyłce, bez zakładania czegokolwiek w ustawieniach. ERP może mieć klienta pod numerem 500, sklep tę samą firmę pod numerem ABC-12, a Intum wie, że to jedna firma, i nie utworzy duplikatu.

**Globalny identyfikator firmy** przychodzi w polu `external_id` i jest zapisywany w kolumnie widocznej w interfejsie: w tabeli klientów, w filtrze zaawansowanym i w eksporcie. To pole na numer, którym posługują się ludzie w firmie, gdy mówią „klient 4471". Jest unikalne w obrębie konta, więc dwa razy tego samego nie wpiszesz przez pomyłkę.

Podać można jedno albo drugie. Przy jednym systemie źródłowym wystarczy `external_id` i wtedy pełni obie role naraz - jest i numerem widocznym w CRM, i kluczem rozpoznawania. Przy kilku źródłach każde wysyła swoje `id`, a `external_id` zostaje na ten jeden numer, który obowiązuje w całej firmie.

Dokładnie tak samo działa to dla osób kontaktowych, nie tylko dla firm.

## Jak to wysłać: trzy przykłady

Wysyłka to jedno zapytanie na adres konta, z tokenem ograniczonym wyłącznie do importu. Nic nie trzeba wcześniej zakładać w ustawieniach poza polami własnymi, jeśli chcesz przesyłać dane spoza standardowych.

```
POST https://twojafirma.intum.pl/automation/import_batches.json
Authorization: Bearer TWOJ_TOKEN
Content-Type: application/json
```

**Jeden system i jeden numer klienta.** Najprostszy wariant, w praktyce wystarcza większości firm:

```json
{
  "source_code": "moj_erp",
  "clients": [
    { "external_id": "4471", "name": "Firma Przykładowa Sp. z o.o.", "tax_no": "5551112233" }
  ]
}
```

Klient dostanie w CRM numer 4471 widoczny w tabeli i filtrach, a przy kolejnej wysyłce zostanie po nim rozpoznany.

**Drugi system, ta sama firma.** Sklep ma własne oznaczenia i nie musi znać numeru z ERP:

```json
{
  "source_code": "sklep",
  "clients": [
    { "id": "ABC-12", "tax_no": "5551112233", "email": "kontakt@firma.pl" }
  ]
}
```

Intum dopasuje firmę po NIP-ie i zapamięta ją pod oznaczeniem ABC-12 w przestrzeni sklepu. Numer widoczny w CRM zostaje ten z ERP, bo sklep go nie przysłał, a pole nieobecne w danych nigdy nie nadpisuje tego, co już jest.

**Firma razem z osobami.** Role dotyczą konkretnej firmy, więc ta sama osoba może być gdzie indziej księgową, a gdzie indziej właścicielem:

```json
{
  "source_code": "moj_erp",
  "clients": [
    {
      "id": "500",
      "external_id": "4471",
      "name": "Firma Przykładowa Sp. z o.o.",
      "contacts_complete": true,
      "contacts": [
        { "id": "u-9", "email": "jan@firma.pl", "first_name": "Jan", "last_name": "Kowalski", "role": "owner" },
        { "id": "u-12", "email": "anna@biuro-ksiegowe.pl", "first_name": "Anna", "role": "accountant" }
      ]
    }
  ]
}
```

Opcja `contacts_complete` mówi, że to pełna lista osób tej firmy: kto na niej nie występuje, przestaje być z nią powiązany. Bez tej opcji można bezpiecznie wysłać jedną zmienioną osobę, a pozostałe zostaną nietknięte. Powiązań dopisanych ręcznie w CRM import nie usuwa nigdy.

W odpowiedzi dostajesz potwierdzenie przyjęcia od razu, a dane pojawiają się w CRM po chwili, gdy bufor zostanie przetworzony. Jeśli połączenie padło i nie wiesz, czy paczka doszła, wyślij ją ponownie: powtórka jest rozpoznawana i pomijana.

## Co z tego wynika w praktyce

Trzy rzeczy, które w innych układach wymagają pracy, tu wychodzą same:

Podłączenie drugiego systemu nie wymaga zakładania nowego pola ani rozmowy o tym, które pole jest czyje. Nowy kod źródła i tyle.

Numer widoczny dla ludzi nie miesza się z numerami technicznymi. Handlowiec w tabeli klientów widzi jeden identyfikator, ten, którego firma faktycznie używa, a nie trzy kolumny z numerami z różnych systemów.

Wiedza o powiązaniach zostaje w CRM, nie w skrypcie integracyjnym. Widać, że ten klient przyszedł z ERP pod numerem 500, i to nie zniknie razem z integracją.

Do tego dochodzi ochrona przed podwójnym przetworzeniem całej paczki danych: przy wysyłce podaje się jej identyfikator, a powtórka po zerwanym połączeniu jest rozpoznawana i pomijana. Ponowienie wysyłki po błędzie jest więc zawsze bezpieczne.

---

## API

## Identyfikatory w API importu

Endpoint: `POST /automation/import_batches.json`, autoryzacja tokenem z uprawnieniem `automation_import`.

### Dwa pola, dwie różne role

| Pole | Gdzie trafia | Rola |
|------|--------------|------|
| `id` | tabela identyfikatorów zewnętrznych, w przestrzeni `source_code` | klucz tożsamości: po nim rozpoznajemy rekord przy kolejnych wysyłkach |
| `external_id` | kolumna `external_id` klienta/kontaktu | globalny numer widoczny w CRM, unikalny w obrębie konta |

Przynajmniej jedno musi być podane. Gdy nie ma `id`, kluczem tożsamości staje się `external_id`.

### Jedno źródło - wystarczy external_id

```json
{
  "kind": "crm_clients",
  "source_code": "moj_erp",
  "batch_id": "2026-08-27-0001",
  "clients": [
    { "external_id": "4471", "name": "Firma Przykładowa Sp. z o.o.", "tax_no": "5551112233" }
  ]
}
```

Klient dostanie w CRM `external_id = 4471` i po tej samej wartości zostanie rozpoznany przy kolejnej wysyłce.

### Kilka źródeł - id osobno dla każdego

ERP wysyła swoje numery i ustawia globalny numer firmy:

```json
{
  "source_code": "moj_erp",
  "clients": [
    { "id": "500", "external_id": "4471", "name": "Firma Przykładowa Sp. z o.o." }
  ]
}
```

Sklep wysyła własny identyfikator tej samej firmy i nie rusza globalnego numeru:

```json
{
  "source_code": "sklep",
  "clients": [
    { "id": "ABC-12", "tax_no": "5551112233" }
  ]
}
```

Oba trafią w tego samego klienta: pierwszy po swoim `id` w przestrzeni `moj_erp`, drugi przez dopasowanie po NIP-ie, po którym zostanie zapamiętany pod `ABC-12` w przestrzeni `sklep`.

### To samo dla osób

```json
{
  "source_code": "moj_erp",
  "clients": [
    {
      "id": "500",
      "contacts": [
        { "id": "u-9", "external_id": "PRAC-9", "email": "jan@firma.pl", "role": "owner" }
      ]
    }
  ]
}
```

### Uwagi

- kolumna `external_id` klienta jest unikalna w obrębie konta - wartość zajęta przez innego klienta odrzuci rekord z komunikatem widocznym w statystykach przebiegu
- pole nieobecne w payloadzie nie nadpisuje wartości w CRM, więc wysyłka jednego pola jest bezpieczna
- `batch_id` chroni przed podwójnym przetworzeniem paczki; bez niego liczony jest hash treści
- `source_code` to stała nazwa systemu (`[a-z][a-z0-9_]*`), której nie należy zmieniać po starcie - to ona wyznacza przestrzeń identyfikatorów