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.
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.
- Raz, przy wdrożeniu: rejestrujesz adres webhooka i zapisujesz jego sekret.
- POST /documents z plikiem PDF i listą sygnatariuszy. Dokument idzie do podpisu od razu.
- Dostajesz zdarzenia o kolejnych krokach: wysłany, otwarty, podpisany.
- Po komplecie podpisów przychodzi document.completed.
- 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.