Zmiana danych
Kto może zapisywać
Klucz zmienia dane tylko wtedy, gdy spełnione są wszystkie warunki:
- Właściciel biura raz włączył zmiany przez API: Ustawienia › API i webhooki › Zmiany przez API. Potrzebny jest potwierdzony adres e-mail, a przy włączonej weryfikacji dwuetapowej sesja potwierdzona kodem. Do tego czasu klucze tylko czytają, a
/mezwracawrites: not_activated. - Klucz ma zakres zapisu danego zasobu. Klucz z dowolnym zakresem zapisu może utworzyć albo wymienić tylko właściciel, który ma włączoną weryfikację dwuetapową i potwierdził to logowanie kodem.
- Twórca klucza mógłby wprowadzić tę samą zmianę w Estalo. Zapis wykonuje się jako ta osoba, według tych samych reguł co aplikacja webowa i mobilna: członkostwo w biurze, rola, przypisane rekordy, limity planu, sprawdzanie duplikatów. Klucz agenta zmienia tylko rekordy agenta. Przypisanie rekordu do kogoś (
assigned_user_id) jest dla właściciela, kierownika i dyrektora w planie dla biur. - Twórca klucza nadal ma włączoną weryfikację dwuetapową. Jeśli ją wyłączy, klucz dalej czyta, ale każdy zapis kończy się
401 api_key_inactive, dopóki jej nie włączy. - Twórca w tej chwili pracuje w biurze, do którego należy klucz. Jeśli przełączył się w Estalo na inne biuro, zapisy kończą się
409 creator_context_mismatch, dopóki nie wróci; nic nie zapisuje się gdzie indziej.
Właściciel może w tym samym miejscu ponownie wyłączyć zmiany (przy włączonej weryfikacji dwuetapowej potrzebne jest logowanie potwierdzone kodem). Klucze z zapisem nadal czytają, każdy zapis kończy się 403 writes_not_enabled, a nowego klucza z zapisem nie da się utworzyć, dopóki zmiany nie zostaną włączone ponownie.
Baza danych sprawdza zakres jeszcze raz przy każdym zapisywanym wierszu, więc klucz bez clients:write nie doda klienta nawet ścieżką kodu, o której byśmy zapomnieli.
Operacje
| Metoda i ścieżka | Działanie | Sukces |
|---|---|---|
POST /api/v1/{zasób} | tworzy rekord | 201, { "data": rekord } |
PATCH /api/v1/{zasób}/{id} | zmienia tylko wysłane pola; null czyści pole tam, gdzie to dozwolone | 200, { "data": rekord } |
DELETE /api/v1/clients/{id} | archiwizuje klienta (status archived, odwracalne) | 200, { "data": { "id": 42, "result": "archived" } } |
DELETE /api/v1/properties/{id} | przenosi ofertę do kosza (do przywrócenia w Estalo przez 30 dni) | 200, result: "trashed" |
DELETE /api/v1/events/{id}, DELETE /api/v1/tasks/{id} | usuwa rekord, tak jak aplikacja | 200, result: "deleted" |
Zapytania i transakcje nie mają DELETE: zmieniasz ich status albo stage. Odpowiedź zawiera rekord w tym samym kształcie co GET, gdy klucz ma też zakres odczytu; w przeciwnym razie tylko { "id": … }.
Treść żądania to obiekt JSON (Content-Type: application/json) o rozmiarze do 64 KB. Pole nieznane albo tylko do odczytu kończy się 400 invalid_request z param; nic nie jest po cichu pomijane. Pola zapisywalne każdego zasobu są w dokumencie OpenAPI (ClientCreate, ClientUpdate, …) i w tabelach na stronie zasobów. W skrócie:
| Zasób | Pola zapisywalne | Wymagane przy POST |
|---|---|---|
| clients | first_name, last_name, name, email, phone, status, type, budget | name albo first_name i last_name |
| properties | title, price, type, transaction_type, market, status, address_city, address_street, address_district, area, rooms, floor, total_floors, build_year, description; assigned_user_id przy PATCH | title, price, type, address_city |
| deals | title, value, stage, client_id, property_id, close_date, assigned_user_id | title, value, stage |
| events | title, type, start_time, end_time, client_id, property_id; outcome przy PATCH | title, start_time, end_time |
| leads | first_name, last_name, customer_name, email, phone, message, source, status; assigned_user_id przy PATCH | customer_name albo first_name i last_name |
| tasks | title, status, priority, task_type, due_date, related_client_id, related_property_id, related_deal_id, assigned_user_id | title |
Uwagi: znaczniki czasu wymagają strefy czasowej. Zadanie łączy się z najwyżej jednym rekordem: wysłanie jednego related_* zastępuje powiązanie, null je usuwa. Ustawienie status zadania na done zapisuje completed_at; outcome wydarzenia zapisuje outcome_at. message zapytania zapisuje się jako treść zapytania, ale GET zwraca ją tylko dla importu maili; source: email_import jest zarezerwowane. Prowizja nie jest częścią API: transakcja utworzona przez API nie ma prowizji, dopóki ktoś jej nie uzupełni w Estalo.
Idempotencja
POST wymaga nagłówka Idempotency-Key; przy PATCH i DELETE jest opcjonalny, ale zalecany. Dla każdej zmiany użyj nowej, losowej wartości (UUID wystarczy, do 128 widocznych znaków ASCII) i używaj jej ponownie tylko przy ponawianiu tej samej zmiany. Przez 24 godziny:
- ten sam klucz z tą samą metodą, ścieżką i treścią zwraca ponownie pierwszą odpowiedź, z nagłówkiem
Idempotent-Replayed: true, i niczego nie zmienia; - ten sam klucz z innym żądaniem kończy się
422 idempotency_key_reused; - dopóki pierwsze żądanie jeszcze trwa,
409 idempotency_in_progress: ponów za chwilę.
Odmowy wydane po przyjęciu żądania (na przykład 409 duplicate albo 402 plan_limit) są zapisywane i odtwarzane tak jak sukcesy. Odpowiedź 5xx nie jest zapisywana, więc możesz ponowić żądanie z tym samym kluczem.
Równoczesne zmiany
PATCH przyjmuje expected_updated_at: wartość updated_at, którą odczytałeś przed podjęciem decyzji o zmianie. Jeśli rekord zmienił się od tego czasu, odpowiedź to 409 conflict z aktualną wartością w details.updated_at; pobierz rekord jeszcze raz i zdecyduj ponownie. Bez tego pola wysłane pola nadpisują to, co jest w rekordzie (pozostałych pól zmiana nie dotyka).
Przykłady
Zastąp klucz i identyfikatory własnymi. Każdy POST potrzebuje własnego Idempotency-Key.
KEY="est_live_xxxxxxxxxxxx_<twój-klucz>"
# Klient
curl -s -X POST https://estalo.pl/api/v1/clients \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"first_name":"Anna","last_name":"Nowak","phone":"+48 600 000 000","type":"buyer"}'
curl -s -X PATCH https://estalo.pl/api/v1/clients/42 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"status":"contacted","expected_updated_at":"2026-11-02T09:15:02.114+00:00"}'
curl -s -X DELETE https://estalo.pl/api/v1/clients/42 -H "Authorization: Bearer $KEY"
# Oferta
curl -s -X POST https://estalo.pl/api/v1/properties \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"2 pokoje, Jeżyce","price":649000,"type":"flat","address_city":"Poznań","area":48.5,"rooms":2}'
curl -s -X DELETE https://estalo.pl/api/v1/properties/0f6c2a1e-0000-4000-8000-000000000000 -H "Authorization: Bearer $KEY"
# Transakcja
curl -s -X POST https://estalo.pl/api/v1/deals \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Jeżyce 2 pokoje","value":649000,"stage":"new","client_id":42,"property_id":"0f6c2a1e-0000-4000-8000-000000000000"}'
curl -s -X PATCH https://estalo.pl/api/v1/deals/7d1e0c2b-0000-4000-8000-000000000000 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"stage":"negotiation"}'
# Wydarzenie w kalendarzu
curl -s -X POST https://estalo.pl/api/v1/events \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Prezentacja","type":"meeting","start_time":"2026-11-05T10:00:00+01:00","end_time":"2026-11-05T11:00:00+01:00","client_id":42}'
# Zapytanie (na przykład z formularza na stronie biura)
curl -s -X POST https://estalo.pl/api/v1/leads \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customer_name":"Jan Kowalski","email":"jan@example.com","message":"Czy oferta jest aktualna?","source":"www"}'
curl -s -X PATCH https://estalo.pl/api/v1/leads/a5d0c3f1-0000-4000-8000-000000000000 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"status":"contacted"}'
# Zadanie
curl -s -X POST https://estalo.pl/api/v1/tasks \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Oddzwonić","task_type":"call","due_date":"2026-11-03T09:00:00Z","related_client_id":42}'
curl -s -X PATCH https://estalo.pl/api/v1/tasks/3b9e1d40-0000-4000-8000-000000000000 \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"status":"done"}'
Błędy
Każdy błąd ma ten sam kształt i stały code:
{ "error": { "code": "insufficient_scope", "message": "…", "request_id": "…" } }
Niektóre błędy mają dodatkowo param (parametr, nagłówek albo pole, którego dotyczą) i details (identyfikatory i nazwy pól, nigdy dane innej osoby).
| HTTP | code | Kiedy |
|---|---|---|
| 400 | invalid_request | błędne id, filtr, limit, kursor, nieznany parametr albo pole nieznane lub tylko do odczytu, brak Idempotency-Key (param to wskazuje) |
| 401 | invalid_api_key | brak klucza, zły format albo nieznany klucz |
| 401 | api_key_expired | klucz po dacie ważności |
| 401 | api_key_inactive | klucz unieważniony albo jego twórca nie może już działać w biurze; przy zapisie także wtedy, gdy twórca wyłączył weryfikację dwuetapową (odczyt działa dalej) |
| 402 | workspace_locked | biuro nie ma aktywnego planu albo jest zawieszone |
| 402 | plan_limit | zapis: osiągnięto limit planu albo plan nie obejmuje funkcji (np. transakcji) |
| 403 | api_not_enabled | beta nie jest włączona dla tego biura |
| 403 | insufficient_scope | klucz nie ma zakresu zasobu (<zasób>:write przy zmianach) |
| 403 | writes_not_enabled | zapis: biuro nie włączyło zmian przez API albo właściciel je wyłączył (odczyt działa dalej) |
| 403 | forbidden | zapis: twórca klucza nie może wprowadzić tej zmiany (np. przypisanie rekordów bez roli kierowniczej w planie dla biur) |
| 404 | not_found | brak rekordu widocznego dla tego klucza albo nieznana ścieżka |
| 405 | method_not_allowed | metoda nie jest dostępna dla tej ścieżki; nagłówek Allow podaje dostępne |
| 409 | conflict | zapis: rekord zmienił się od Twojego expected_updated_at; details.updated_at to aktualna wartość |
| 409 | duplicate | zapis: istnieje klient z tym telefonem lub e-mailem; details.duplicate ma jego field i id |
| 409 | creator_context_mismatch | zapis: twórca klucza pracuje teraz w innym biurze |
| 409 | idempotency_in_progress | zapis: żądanie z tym Idempotency-Key jeszcze trwa |
| 413 | payload_too_large | zapis: treść ponad 64 KB |
| 415 | unsupported_media_type | zapis: treść nie została wysłana jako application/json |
| 422 | validation_failed | zapis: wartość jest nieprawidłowa; details.fields (albo details.reason) mówi która |
| 422 | idempotency_key_reused | zapis: ten Idempotency-Key użyto z innym żądaniem |
| 429 | rate_limited | patrz niżej; odczekaj Retry-After sekund |
| 500 | internal_error | błąd po naszej stronie; podaj request_id, jeśli się z nami kontaktujesz |
Rozgałęziaj logikę po code, nie po message. Odpowiedzi mają Cache-Control: no-store i X-Request-Id.