Dla integratorów

Dokumentacja API podpiszmy.pl

Umowa powstaje w Twoim systemie, my wysyłamy ją do podpisu, a podpisany plik wraca do Ciebie. Bez klikania w panelu.

API i webhooki działają w planie Business oraz Team. Plan wybierzesz na stronie z cennikiem. Token wygenerujesz w panelu, w sekcji API i integracje.

Uwierzytelnianie

Każde zapytanie niesie token osobisty w nagłówku Authorization. Token tworzysz i unieważniasz w panelu, w sekcji API i integracje. Pokazujemy go jeden raz, przy utworzeniu, i nie mamy sposobu, żeby pokazać go ponownie. Token działa wyłącznie na koncie, na którym powstał, i widzi dokładnie te dokumenty, które widzi to konto w przeglądarce.

Odpowiedzi są zawsze w formacie JSON, także przy błędzie. Warto wysyłać nagłówek Accept: application/json, ale nie jest on wymagany.

curl -s https://podpiszmy.pl/api/v1/me \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -H "Accept: application/json"

Adres bazowy to https://podpiszmy.pl/api/v1. Wersja siedzi w adresie, więc kolejne wydanie API nie zepsuje działającej integracji.

Typowy obieg

Najczęstszy scenariusz wygląda tak. W Twoim systemie powstaje umowa (oferta zaakceptowana, zlecenie przyjęte, nowy współpracownik). Wysyłasz ją jednym zapytaniem, my zajmujemy się resztą, a po komplecie podpisów przychodzi do Ciebie zdarzenie i pobierasz podpisany plik.

  1. Raz, przy wdrożeniu: rejestrujesz adres webhooka i zapisujesz jego sekret.
  2. POST /documents z plikiem PDF i listą sygnatariuszy. Dokument idzie do podpisu od razu.
  3. Dostajesz zdarzenia o kolejnych krokach: wysłany, otwarty, podpisany.
  4. Po komplecie podpisów przychodzi document.completed.
  5. GET /documents/{uuid}/pdf zwraca podpisany plik, a /audyt ślad audytowy do Twojej bazy.

Wysłanie dokumentu do podpisu

Jedno zapytanie tworzy dokument, zaprasza sygnatariuszy i wysyła im wiadomość z linkiem. Plik przesyłasz jako multipart/form-data. Maksymalny rozmiar PDF to 6 MB.

curl -s https://podpiszmy.pl/api/v1/documents \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -F "[email protected]" \
  -F "title=Umowa o dzieło nr 14/2026" \
  -F "message=W razie pytań proszę o kontakt." \
  -F "signing_mode=sequential" \
  -F "signers[0][name]=Anna Nowak" \
  -F "signers[0][email][email protected]" \
  -F "signers[1][name]=Jan Kowalski" \
  -F "signers[1][email][email protected]" \
  -F "signers[1][require_sms]=1" \
  -F "signers[1][phone]=+48600000000"

Pola: file (PDF, wymagane), title (wymagane, do 180 znaków), message (opcjonalna wiadomość do sygnatariuszy), signing_mode (sequential po kolei albo parallel równolegle, domyślnie po kolei), signers (od jednego do piętnastu, każdy z name i email, opcjonalnie require_sms i phone oraz require_id_check, opisane niżej), send (domyślnie true; ustaw false, żeby przygotować dokument i wysłać go osobnym zapytaniem).

Potwierdzenie kodem SMS i weryfikacja tożsamości

require_sms (1 albo true): przed podpisem sygnatariusz wpisuje jednorazowy kod wysłany SMS-em. Wymaga pola phone: polski numer komórkowy, 9 cyfr (600000000) albo z kierunkowym (+48600000000), spacje i myślniki są dopuszczalne. Inny numer albo brak numeru daje odpowiedź 422 z błędem pola signers.N.phone, a dokument nie powstaje.

require_id_check: przed podpisem sygnatariusz przesyła zdjęcie dokumentu tożsamości (przód i tył) oraz zdjęcie twarzy, a podpis jest możliwy po pozytywnej ocenie. To osobna metoda niż kod SMS; można włączyć jedną albo obie.

W odpowiedzi każdy sygnatariusz ma pola require_sms, sms_verified (kod wpisany poprawnie), require_id_check, identity_verified (weryfikacja dokumentem zaliczona) i identification_methods (np. ["link e-mail", "kod SMS"]). Pole dokumentu signature_level ma wartość ades, gdy każda podpisana osoba potwierdziła się kodem SMS albo weryfikacją tożsamości (podpis zaawansowany), w przeciwnym razie ses. W śladzie audytowym (GET /documents/{uuid}/audyt) są zdarzenia z polem typ równym sms_sent i sms_verified, a Karta Podpisów w PDF podaje metodę identyfikacji każdej osoby.

HTTP/1.1 201 Created

{
  "data": {
    "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "title": "Umowa o dzieło nr 14/2026",
    "status": "sent",
    "effective_status": "sent",
    "status_label": "Wysłany",
    "signing_mode": "sequential",
    "sent_at": "2026-09-19T12:40:11+02:00",
    "expires_at": "2026-10-03T12:40:11+02:00",
    "signed_count": 0,
    "pdf_url": "https://podpiszmy.pl/api/v1/documents/9f1c2d3e.../pdf",
    "audit_url": "https://podpiszmy.pl/api/v1/documents/9f1c2d3e.../audyt",
    "signers": [
      { "name": "Anna Nowak", "email": "[email protected]", "status": "pending", "signed_at": null,
        "identity_verified": false, "require_id_check": false, "require_sms": false, "sms_verified": false,
        "identification_methods": ["link e-mail"] },
      { "name": "Jan Kowalski", "email": "[email protected]", "status": "pending", "signed_at": null,
        "identity_verified": false, "require_id_check": false, "require_sms": true, "sms_verified": false,
        "identification_methods": ["link e-mail"] }
    ],
    "signature_level": "ses"
  }
}

Punkty końcowe

Metoda i adres Co robi
GET /me Dane konta, plan i obowiązujące limity zapytań.
GET /documents Lista dokumentów. Filtr ?status=draft|sent|in_progress|completed|rejected|cancelled|expired|w_toku, wyszukiwanie ?q=, stronicowanie ?per_page= (do 100).
POST /documents Tworzy dokument i domyślnie od razu wysyła go do podpisu.
GET /documents/{uuid} Status jednego dokumentu razem ze stanem każdego sygnatariusza.
POST /documents/{uuid}/send Wysyła dokument przygotowany wcześniej z send=false.
POST /documents/{uuid}/cancel Anuluje dokument. Wysłane linki do podpisu przestają działać. Opcjonalne pole reason.
POST /documents/{uuid}/reminders Przypomina o podpisie. Bez pola email przypomina wszystkim, na których czekamy.
GET /documents/{uuid}/pdf Podpisany PDF. Parametr ?wariant=oryginal zwraca plik bez podpisów.
GET /documents/{uuid}/audyt Ślad audytowy jako dane, razem z wynikiem sprawdzenia łańcucha skrótów.
GET /contacts Kontakty konta. Wyszukiwanie ?q=, dokładny adres ?email=, sortowanie ?sort=name|-name|created_at|-created_at|last_used_at|-last_used_at, stronicowanie ?per_page= (do 100).
GET /contacts/{id} Jeden kontakt.
POST /contacts Dodaje kontakt. Pola: name i email (wymagane), phone, company, note. Adres musi być unikalny w kontaktach konta.
PATCH /contacts/{id} Zmienia podane pola kontaktu. Pola pominięte zostają bez zmian.
DELETE /contacts/{id} Usuwa kontakt na stałe, bez kopii. Dokumenty z tą osobą zostają.
GET /webhooks Lista zarejestrowanych adresów i katalog dostępnych zdarzeń.
POST /webhooks Rejestruje adres. Odpowiedź zawiera sekret, pokazywany tylko ten jeden raz.
PATCH /webhooks/{id} Zmienia listę zdarzeń albo wyłącza adres.
DELETE /webhooks/{id} Usuwa adres.
POST /webhooks/{id}/test Wysyła zdarzenie próbne, żeby sprawdzić podpis bez czekania na umowę.
GET /webhooks/{id}/dostarczenia Ostatnie próby dostarczenia razem z kodami odpowiedzi i błędami.
# status jednego dokumentu
curl -s https://podpiszmy.pl/api/v1/documents/UUID \
  -H "Authorization: Bearer TWOJ_TOKEN"

# tylko te, na które jeszcze czekamy
curl -s "https://podpiszmy.pl/api/v1/documents?status=w_toku" \
  -H "Authorization: Bearer TWOJ_TOKEN"

# podpisany plik prosto na dysk
curl -s https://podpiszmy.pl/api/v1/documents/UUID/pdf \
  -H "Authorization: Bearer TWOJ_TOKEN" -o umowa-podpisana.pdf

# ślad audytowy do zapisania u siebie
curl -s https://podpiszmy.pl/api/v1/documents/UUID/audyt \
  -H "Authorization: Bearer TWOJ_TOKEN"

# przypomnienie do jednej osoby
curl -s -X POST https://podpiszmy.pl/api/v1/documents/UUID/reminders \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'

# anulowanie
curl -s -X POST https://podpiszmy.pl/api/v1/documents/UUID/cancel \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Zlecenie wycofane przez klienta"}'

Podpisany plik powstaje w chwili złożenia ostatniego podpisu. Wcześniej /pdf odpowiada kodem 409 i mówi, ilu podpisów brakuje, zamiast oddawać plik bez podpisów.

Kontakty

Kontakty to ta sama książka adresowa co zakładka Kontakty w panelu. Każdy sygnatariusz wysłanego dokumentu trafia do niej sam, także dokumentu wysłanego przez API. Adres zapisujemy małymi literami i jest unikalny w kontaktach konta (na planie Team: w kontaktach całej organizacji), więc drugi POST z tym samym adresem dostaje 422. Telefon zapisujemy jako same cyfry, na przykład 600000000. Dodawanie, zmiana i usuwanie liczą się do limitu zapisów.

# dodanie kontaktu
curl -s -X POST https://podpiszmy.pl/api/v1/contacts \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Anna Nowak","email":"[email protected]","phone":"600 000 000","company":"Nowak i Wspólnicy"}'

# czy ta osoba jest już w kontaktach
curl -s "https://podpiszmy.pl/api/v1/[email protected]" \
  -H "Authorization: Bearer TWOJ_TOKEN"

# zmiana firmy
curl -s -X PATCH https://podpiszmy.pl/api/v1/contacts/ID \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"company":"Nowak Sp. z o.o."}'

# usunięcie
curl -s -X DELETE https://podpiszmy.pl/api/v1/contacts/ID \
  -H "Authorization: Bearer TWOJ_TOKEN"
{
  "data": {
    "id": 42,
    "name": "Anna Nowak",
    "email": "[email protected]",
    "phone": "600000000",
    "company": "Nowak i Wspólnicy",
    "note": null,
    "shared_with_team": false,
    "last_used_at": null,
    "created_at": "2026-10-05T12:00:00+02:00",
    "updated_at": "2026-10-05T12:00:00+02:00"
  }
}

Webhooki

Zamiast odpytywać nas w pętli, podaj adres, na który mamy wysyłać zdarzenia. Wysyłamy je metodą POST, w formacie JSON, wyłącznie na publiczne adresy https.

curl -s -X POST https://podpiszmy.pl/api/v1/webhooks \
  -H "Authorization: Bearer TWOJ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://twoj-system.pl/hooki/podpiszmy",
        "events": ["document.sent", "document.signed", "document.completed", "document.rejected"]
      }'
Zdarzenie Kiedy przychodzi
document.sent Dokument wysłany do podpisu
document.viewed Sygnatariusz otworzył dokument
document.signed Jeden z sygnatariuszy podpisał
document.rejected Dokument odrzucony
document.expired Minął termin podpisania
document.cancelled Dokument anulowany przez nadawcę
document.completed Dokument podpisany przez wszystkich

Treść zdarzenia

POST /hooki/podpiszmy
Content-Type: application/json
X-Podpiszmy-Event: document.completed
X-Podpiszmy-Signature: sha256=7b1c...
X-Podpiszmy-Delivery: 2f9a1c40-1d8e-4d21-9f4e-6a0b0c7d2e11
X-Podpiszmy-Attempt: 1

{
  "event": "document.completed",
  "created_at": "2026-09-19T13:02:44+02:00",
  "data": {
    "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "title": "Umowa o dzieło nr 14/2026",
    "status": "completed",
    "completed_at": "2026-09-19T13:02:41+02:00",
    "pdf_url": "https://podpiszmy.pl/api/v1/documents/9f1c2d3e.../pdf",
    "audit_url": "https://podpiszmy.pl/api/v1/documents/9f1c2d3e.../audyt",
    "signers": [
      { "name": "Anna Nowak", "email": "[email protected]", "status": "signed", "signed_at": "2026-09-19T13:02:41+02:00" }
    ]
  }
}

Podpisany plik w treści zdarzenia

Każde zdarzenie niesie pdf_url i audit_url, więc plik pobierzesz jednym żądaniem tym samym tokenem, którym rejestrowałeś webhook. Jeśli Twój system ma zapisać umowę od razu, bez drugiego żądania, zaznacz przy webhooku „Przyślij podpisany plik w treści zdarzenia” (w panelu albo polem dolacz_plik przy rejestracji). Do document.completed dokładamy wtedy dwa pola:

"plik": {
  "dolaczony": true,
  "nazwa": "podpisany-9f1c2d3e-....pdf",
  "content_type": "application/pdf",
  "rozmiar": 284119,
  "sha256": "3d51f5...cfd9a",
  "base64": "JVBERi0xLjQKJ..."
},
"audyt": {
  "uuid": "9f1c2d3e-...",
  "final_sha256": "3d51f5...cfd9a",
  "lancuch_nienaruszony": true,
  "zdarzenia": [ { "typ": "signed", "ip": "83.21.140.9", "czas": "2026-09-19T13:02:41+02:00", "chain_hash": "..." } ]
}

Porównaj sha256 z polem final_sha256 ze śladu audytowego: zgodność jest dowodem, że plik doszedł w całości. Plik idzie w base64, czyli rośnie o mniej więcej jedną trzecią, a odbiorcy za nginxem mają zwykle limit ciała żądania równy 1 MB. Dlatego doklejamy tylko pliki do 3 MB. Powyżej tego progu zdarzenie przychodzi bez pliku, ale z polem plik.dolaczony: false i powodem, a plik pobierzesz spod pdf_url. Nic nie ginie po cichu.

Sprawdzenie podpisu

Nagłówek X-Podpiszmy-Signature to HMAC-SHA256 policzony z surowego ciała zapytania i Twojego sekretu, w postaci sha256=HEX. Licz go przed sparsowaniem JSON-a. Przepuszczenie treści przez dekoder i koder zmienia bajty, a wtedy podpis przestaje pasować, mimo że wszystko jest w porządku. Porównuj podpisy funkcją odporną na pomiar czasu.

// PHP
$body = file_get_contents('php://input');
$oczekiwany = 'sha256=' . hash_hmac('sha256', $body, $sekret);

if (! hash_equals($oczekiwany, $_SERVER['HTTP_X_PODPISZMY_SIGNATURE'] ?? '')) {
    http_response_code(400);
    exit;
}

$zdarzenie = json_decode($body, true);
# Python
import hmac, hashlib

oczekiwany = "sha256=" + hmac.new(sekret.encode(), body, hashlib.sha256).hexdigest()

if not hmac.compare_digest(oczekiwany, naglowki.get("X-Podpiszmy-Signature", "")):
    return 400

Ponowienia

Odpowiedz kodem 2xx, gdy przyjmiesz zdarzenie. Każda inna odpowiedź i każdy brak połączenia uruchamiają ponowienie: łącznie pięć podejść z odstępami 10 sekund, minuta, 5 minut i 30 minut. Nagłówek X-Podpiszmy-Delivery jest ten sam we wszystkich podejściach tego samego zdarzenia, więc użyj go do odsiania duplikatów. Wszystkie próby razem z kodami odpowiedzi widzisz w panelu oraz pod GET /webhooks/{id}/dostarczenia.

Wyzwalacze bez pisania kodu

Punkty opisane wyżej są dla programisty. Wyzwalacz jest dla wszystkich pozostałych narzędzi: CRM-a, który umie zawołać adres po zmianie etapu, Zapiera, Make, n8n, formularza. Zapisujesz umowę jako szablon w panelu, w sekcji API i integracje tworzysz wyzwalacz i dostajesz jeden adres. Od tej chwili wystarczy, że Twoje narzędzie wyśle pod niego żądanie POST z adresem e-mail osoby podpisującej. Plik, treść i kolejność podpisywania stoją w szablonie, więc niczego nie wgrywasz i nie przepisujesz.

curl -s -X POST https://podpiszmy.pl/api/v1/wyzwalacz/TWOJ_KLUCZ \
  -H "Content-Type: application/json" \
  -d '{
        "imie_nazwisko": "Jan Kowalski",
        "email": "[email protected]",
        "tytul": "Umowa o dzieło nr 14/2026"
      }'

HTTP/1.1 201 Created
{
  "data": {
    "uuid": "9f1c2d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f",
    "title": "Umowa o dzieło nr 14/2026",
    "status": "sent",
    "pdf_url": "https://podpiszmy.pl/api/v1/documents/9f1c2d3e.../pdf",
    "audit_url": "https://podpiszmy.pl/api/v1/documents/9f1c2d3e.../audyt",
    "signers": [ { "name": "Jan Kowalski", "email": "[email protected]", "status": "pending" } ]
  }
}

Pola tytul i wiadomosc są nieobowiązkowe: bez nich bierzemy je z szablonu. Zamiast jednej osoby możesz podać listę sygnatariusze w postaci [{"name": "...", "email": "..."}], do 15 pozycji, w kolejności podpisywania. Gdy nie podasz nikogo, umowa idzie do stałych sygnatariuszy zapisanych w szablonie.

Gdy szablon ma pola danych (na przykład nazwa klienta, numer umowy, nowa stawka), podajesz ich wartości w obiekcie pola, z kluczami takimi jak nazwy pól w szablonie: {"pola": {"Nazwa klienta": "...", "Numer umowy": "..."}}. Każde pole jest wymagane. Brak wartości kończy się odpowiedzią 422 z listą oczekiwane_pola, a dokument nie powstaje. Wartości trafiają na kopię pliku szablonu, sam szablon zostaje bez zmian.

Adres wyzwalacza jest sekretem: kto go zna, ten może wystawić umowę na Twoim koncie. Pokazujemy go jeden raz, przy utworzeniu, trzymamy u siebie wyłącznie jego skrót, a w panelu wyłączysz go i usuniesz jednym kliknięciem. Jeśli Twoje narzędzie potrafi liczyć HMAC, dołóż nagłówek X-Podpiszmy-Signature w tej samej postaci, co przy webhookach, tyle że z sekretem wyzwalacza. Wtedy sprawdzimy go i odrzucimy każde żądanie z błędnym podpisem. Wyzwalacz przyjmuje do 20 wywołań na minutę i działa w planach z dostępem do API, a po zejściu z planu odpowiada kodem 402 zamiast wysyłać umowy.

Limity zapytań

Limity liczymy na konto, nie na adres IP, więc integracja na współdzielonym hostingu nie konkuruje z cudzym ruchem. Odczyt: 120 zapytań na minutę. Zapis, czyli tworzenie dokumentów, wysyłka, anulowanie, przypomnienia, zmiany webhooków i kontaktów: 20 na minutę. Po przekroczeniu dostajesz kod 429 razem z nagłówkiem Retry-After. Bieżące zużycie widzisz w panelu.

Kody odpowiedzi

Kod Znaczenie
200, 201 Zapytanie wykonane.
401 Brak tokena, token odwołany albo nieprawidłowy.
402 Konto nie ma planu z dostępem do API. Odpowiedź podaje nazwę planu i adres cennika.
404 Nie ma takiego dokumentu na tym koncie. Tego samego kodu używamy dla cudzych dokumentów, żeby nie potwierdzać, że istnieją.
409 Dokument jest w stanie, w którym ta operacja nie ma sensu. Na przykład podpisany plik przed kompletem podpisów.
422 Dane zapytania są nieprawidłowe albo wyczerpała się pula dokumentów.
429 Przekroczony limit zapytań tego konta albo przypomnienie wysłane zbyt szybko po poprzednim.

Podłącz swój system do podpisu

API i webhooki działają w planie Business. Umowy wychodzą z Twojego systemu, a podpisany plik wraca do niego automatycznie.