Webhooks otrzymuj wydarzenia podpisu w czasie rzeczywistym
Skonfiguruj adres URL HTTPS na pulpicie nawigacyjnym Certyneo i otrzymuj POST podpisany HMAC-SHA256 za każdym razem, gdy na twoich kopertach nastąpi zdarzenie: podpis, odmowa, wygaśnięcie. 11 obsługiwanych zdarzeń, 5 prób dostarczenia z backoff'em wykładniczym, weryfikacja kryptograficzna w 8 liniach kodu.
< 5s
Średni czas do dostarczenia po zdarzeniu
5x
Próby dostarczenia w sumie, rozłożone na około 1 h 20
HMAC-SHA256
Algorytm podpisu każdego żądania
Katalog wydarzeń
12 zdarzeń poniżej obejmuje cały cykl życia koperty Certyneo. Aktywuj te, które Cię interesują w Ustawienia → Webhook'i, zignoruj resztę — subskrypcja jest granularna na zdarzenie.
| Wydarzenie | Wyzwanie |
|---|---|
envelope.created | Powstaje otoczka (za pomocą UI, API lub szablonu) użyteczna do synchronizacji zapisu po stronie CRM od momentu jego utworzenia. |
envelope.sent | Koperta jest wysyłana do sygnatariuszy (pierwszy wysłany e-mail). |
envelope.completed | Wszyscy sygnatariusze podpisali i zapieczętowany PDF eIDAS jest przechowywany. Payload zawiera signedDocumentUrl, link przedsignowany ważny przez 7 dni; w przeciwnym razie GET /v1/envelopes/id/signed-document oraz ścieżka audytu via GET /v1/envelopes/id/audit-trail. |
envelope.declined | Sygnatariusz odmówił koperty. Adres odmawiającego znajduje się w `data.declinedBy`, a powód, jeśli został wprowadzony, w `data.reason`. |
envelope.voided | Księga została anulowana przez wystawcę przed podpisaniem. |
envelope.expired | Data wygaśnięcia koperty minęła bez pełnego podpisu. Sprawdź GET /v1/envelopes/id, aby poznać brakujących sygnatariuszy. |
envelope.returned_to_sender | Sygnatariusz zwrócił kopertę do emitenta w celu skorygowania, bez jej odrzucenia. Powód znajduje się w `data.reason`, a autor zwrotu w `data.returnedBy`. |
envelope.resubmitted | Emitent skorygował i wysłał ponownie kopertę, która została wcześniej zwrócona. Oznacza wznowienie cyklu podpisywania. |
recipient.signed | Indywidualny sygnatariusz podpisał (ale niekoniecznie wszyscy). Przydatne do śledzenia postępu i przechodzenia do następnego etapu sekwencyjnego workflow'u. Uwaga: zapieczętowany PDF jeszcze nie istnieje na tym etapie, nawet dla ostatniego sygnatariusza — pobieranie uruchomione tutaj zwraca HTTP 409. Użyj envelope.completed dla dokumentu. |
recipient.viewed | Podpisujący otworzył linki do podpisu, nie podpisując jeszcze, co jest przydatne w celu ukierunkowanego wprowadzania w życie. |
recipient.approved | Osoba zatwierdzająca zatwierdziła kopertę bez złożenia na niej podpisu (workflow wewnętrznego zatwierdzenia). Adres znajduje się w `data.approvedBy`. |
recipient.bounced | Serwer poczty odbiorcy ostatecznie odrzucił zaproszenie lub ponowne wysłanie (skrzynka nieistniejąca, domena martwa). Odbiorca zmienia status na BOUNCED i automatyczne ponowne próby się zatrzymują. Adres znajduje się w `data.recipientEmail`: popraw go i wyślij ponownie kopertę. |
recipient.signed nie oznacza "dokument dostępny"
recipient.signed jest emitowany dla KAŻDEGO sygnatariusza w momencie, gdy zakończy on podpisywanie — w tym ostatni, przed zmontowaniem i przechowaniem zapieczętowanego pliku PDF. Pobieranie wyzwolone z tego handlera zawsze otrzyma HTTP 409 "Signed document not available until the envelope is COMPLETED". To nie jest błąd: to "nie gotowe jeszcze". Subskrybuj envelope.completed, aby pobrać dokument, i zachowaj recipient.signed do śledzenia postępu (kto podpisał i kiedy).
Format ładunku użytecznego
Wszystkie dostarczenia udostępniają ten sam schemat JSON najwyższego poziomu: `event`, `data` i `timestamp`. Nazwa zdarzenia jest również powtarzana w nagłówku `X-Certyneo-Event`, co pozwala na routing przed nawet parsowaniem treści. Zawartość `data` różni się w zależności od zdarzenia, ale zawsze pozostaje płaskim obiektem prostych wartości — nigdy tablica ani zagnieżdżony obiekt. Oto kompletne dostarczenie `envelope.completed`.
POST /webhooks/certyneo HTTP/1.1
Host: your.app
Content-Type: application/json
X-Certyneo-Event: envelope.completed
X-Certyneo-Delivery-Id: cm8f2h6a10004qr9k5p2wm3xt
X-Certyneo-Signature: 4f3d1c8b2a9e7f60d5c4b3a2918e7f6d5c4b3a2918e7f6d5c4b3a2918e7f6d5c{
"id": "cm8f2h6a10004qr9k5p2wm3xt",
"event": "envelope.completed",
"data": {
"envelopeId": "cm7x2k9p40001qz8h3f7bn2ld",
"sandbox": false,
"subject": "Contrat de prestation Acme Corp",
"status": "COMPLETED",
"signatureLevel": "ADVANCED",
"aesSignerCount": 2,
"completedAt": "2026-05-27T08:42:13.000Z",
"recipientCount": 2,
"recipients": [
{ "email": "alice@acme.com", "name": "Alice Martin", "role": "SIGNER", "status": "SIGNED" },
{ "email": "bob@acme.com", "name": "Bob Durand", "role": "SIGNER", "status": "SIGNED" }
],
"signedDocumentUrl": "https://storage.certyneo.com/signed/cm7x2k9p4.../contrat.pdf?X-Amz-Expires=604800&X-Amz-Signature=..."
},
"timestamp": "2026-05-27T08:42:13.521Z"
}signedDocumentUrl to link wstępnie podpisany (pre-signed) ważny przez 7 dni od zdarzenia: unika drugiego uwierzytelnionego wywołania w celu pobrania pliku PDF. Jest nieobecny — a nie null — jeśli wstępne podpisanie nie powiodło się lub na kopercie QES ukończonej bez przechowanego dokumentu; powrót przez GET /v1/envelopes/id/signed-document, który pozostaje źródłem prawdy. Uważaj również na ręczne odtworzenie z martwej kolejki więcej niż 7 dni po zdarzeniu: link w payload'u jest wygasły, endpoint API nie.
Podpis HMAC jest obliczany na ciele surowym wysyłanym nie zmieniaj spacji, re-parsing JSON często zmienia kolejność kluczy i łamię weryfikację.
Sprawdź podpis HMAC
Każde żądanie jest podpisane twoją tajną kluczem webhook'a (wyświetlanym tylko raz podczas tworzenia subskrypcji). Podpis jest transmitowany w nagłówku `X-Certyneo-Signature`: jest to HMAC-SHA256 surowej treści żądania, zakodowany szesnastkowo, bez prefiksu ani sygnatury czasowej. ZAWSZE weryfikuj podpis przed przetworzeniem payload'u — bez tego kroku każdy może sfałszować zdarzenie i wywołać twój endpoint.
Node.js / TypeScript
import crypto from "node:crypto";
export function verifyCertyneoSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
// X-Certyneo-Signature = HMAC-SHA256(secret, rawBody), hex-encoded.
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = Buffer.from(signatureHeader, "hex");
const computed = Buffer.from(expected, "hex");
// timingSafeEqual throws when the two buffers differ in length —
// a malformed header must return false, not crash the handler.
if (received.length !== computed.length) return false;
// timingSafeEqual to mitigate timing attacks.
return crypto.timingSafeEqual(computed, received);
}Python
import hashlib
import hmac
def verify_certyneo_signature(
raw_body: bytes, signature_header: str, secret: str
) -> bool:
"""Verify a Certyneo webhook signature.
`X-Certyneo-Signature` holds the hex-encoded HMAC-SHA256 of the
raw request body, computed with your webhook secret.
"""
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)Częste błędy
Nie używaj `===` lub `==` do porównania spodziewanego podpisu z otrzymanym. Użyj funkcji time-safe (`crypto.timingSafeEqual` w Node, `hmac.compare_digest` w Python). W przeciwnym razie różnica czasu porównania między dwoma podpisami stopniowo ujawnia sekret cierpliwemu atakującemu (timing attack).
Polityka ponownego próbkowania
Jeśli Twój endpoint zbyt długo odpowiada, odrzuca połączenie lub zwraca 5xx (lub 429), ponownie próbujemy zgodnie z wykładniczym wycofaniem: 5 prób łącznie, przez około 1 h 20. Po piątej próbie zdarzenie trafia do kolejki wiadomości utraconych — dostępne i odtwarzalne z Twojego dashboardu, ale nie ponawiane automatycznie. Jawny refusal (401, 403, 404, 410, 422…) nigdy nie jest ponawiane: odpowiedź się nie zmieni, zdarzenie trafia bezpośrednio do kolejki wiadomości utraconych.
| Próba | Przedział przed próbą | Upływający czas od zdarzenia |
|---|---|---|
| #1 | 0 | 0 |
| #2 | + 1 min | 1 min |
| #3 | + 5 min | 6 min |
| #4 | + 15 min | 21 min |
| #5 | + 1 h | 1 h 21 |
Wskazane opóźnienia są minimami: skanowanie ponowienia próby jest synchronizowane przez cron, więc próba może zostać wysłana nieco później niż teoretyczny czas. Porzucone zdarzenia znajdują się w Webhooks → Niepowodzenia z przyciskiem ręcznego odtworzenia i bez limitu okresu przechowywania. Endpoint, który doznaje pięciu definitywnych niepowodzeń — lub który zwraca 404 / 410 — jest automatycznie dezaktywowany i jesteś o tym powiadamiany e-mailem: reaktywuj go po naprawie, licznik restartuje od zera (każde udane dostarczenie również resetuje go do zera).
Testy bez wysyłania prawdziwej koperty
Najpierw utwórz subskrypcję — odpowiedź zawiera `secret` niezbędny do weryfikacji HMAC, wyświetlany tylko raz. Z menu Ustawienia → Webhooks przycisk „Testuj" wysyła następnie POST podpisany dokładnie jak rzeczywista dostawa i wyświetla surową odpowiedź Twojego serwera: to najszybszy sposób na walidację Twojego handlera, lokalnie przez ngrok lub w ciągłej integracji.
# Create a subscription — "secret" is returned once, store it now.
curl -X POST https://api.certyneo.com/v1/webhooks \
-H "Authorization: Bearer $CERTYNEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your.app/webhooks/certyneo",
"events": ["envelope.completed", "recipient.signed"]
}'URL musi być publicznie dostępny przez HTTPS: prywatny adres lub localhost zostanie odrzucony przez ochronę SSRF w momencie tworzenia subskrypcji.
Subskrypcje są oddzielone dla każdego środowiska, jak klucze: webhook utworzony za pomocą klucza sk_test_ otrzymuje tylko zdarzenia z kopert testowych, webhook utworzony za pomocą klucza sk_live_ tylko zdarzenia z prawdziwych kopert. Koperta testowa nigdy więc nie dociera do Twojego adresu URL produkcji, a każdy ładunek wskazuje „sandbox" (true lub false).
6 praktyk, które należy przestrzegać
- Sprawdź sygnaturę HMAC PREZ każdej lekturze ciała użyj porównania bezpiecznego w czasie.
- Deduplikuj na podstawie nagłówka `X-Certyneo-Delivery-Id` (identyczne z `id` w treści), przechowując już widoczne identyfikatory w bazie — powtórzenie może dostarczyć zdarzenie, które już przetwarzałeś, jeśli twoja odpowiedź 2xx została utracona, z tym samym identyfikatorem przy każdej próbie.
- Odpowiadać HTTP 2xx w ciągu maksymalnie 10 sekund, następnie przetwarzać asynchronicznie (queue). Poza tym dostarczanie jest przerwane i liczone jako błąd.
- Logging body brut + pełny podpis w debugu weryfikacja HMAC często nie działa na BOM lub niewidocznym białym przestrzeni.
- Monitoruj stronę Webhooks → Niepowodzenia: jesteś powiadamiany e-mailem, jeśli Twój endpoint jest dezaktywowany, ale nie zdarzenie po zdarzeniu — zdarzenie, które wyczerpie swoje próby, to Ty masz go odtworzyć.
- Filtrować zdarzenia przy subskrypcji zamiast w twoim handlerze i pozostać poniżej limitu 5 subskrypcji na konto.
Nie jesteś zobowiązany do hostowania punktu końcowego, aby otrzymywać te zdarzenia: łącznik ustanawia subskrypcję dla Ciebie i uruchamia przepływ bezpośrednio na zdarzeniu koperty. Patrz Certyneo dla Power Automate i Microsoft 365.
Do dalszego rozwoju
Gotowi do podłączenia systemów?
Webhooks i REST API są zawarte od planu Standard. Utwórz swoje konto, wygeneruj klucz `sk_test_` i podłącz swój endpoint w sandbox zanim przejdziesz na produkcję.