Webhooks
Everything you need to know about setting up and using webhooks in Preserve
What is a webhook?
A webhook is an automatic notification that Preserve sends to your own systems the moment something happens — like when a document is uploaded or a certificate is issued.
Without webhooks, your system would need to repeatedly ask Preserve "has anything changed?" (called polling). Webhooks flip that around: instead of your system asking, Preserve tells you instantly, in real time.
Think of it like signing up for a text alert instead of refreshing your inbox every few minutes. You provide a URL, and whenever a relevant event occurs, Preserve sends a JSON message to that URL automatically.
How does the full flow work, step by step?
The lifecycle of every webhook delivery has four steps:
- Register — A platform admin adds your endpoint URL to Preserve, giving the webhook a name so you can identify it later.
- Event — Something happens in Preserve that generates a webhook event (e.g. a user earns a certificate).
- Deliver — Preserve POSTs a JSON payload to every registered webhook URL, typically within seconds.
- Acknowledge — Your server returns any 2xx HTTP status code (e.g. 200 OK). Preserve sees that and considers the delivery successful.
Once you acknowledge, your side can process the data however you like in the background.
Who is responsible for setting up webhooks?
Webhooks are managed per organization by a platform admin. Developers typically build and host the endpoint that receives events, but an admin is needed to register it inside Preserve.
When registering, three fields are required:
- Name — A label you choose (e.g. "HR System Integration"). This appears in every delivery payload so your code can tell which webhook fired.
- Description — Optional. Internal notes for your team about what this webhook is used for.
- URL — The publicly reachable HTTPS address Preserve will POST events to. Must be HTTPS — plain HTTP is rejected at registration.
Do I need coding knowledge to use webhooks?
Setting up the registration in Preserve requires no coding — a platform admin can do that through the interface. However, receiving and processing webhook events does require a developer.
Someone on your team needs to build and host an HTTPS endpoint that can receive POST requests, parse the JSON payload and route logic based on the event type, return a 2xx response quickly and handle processing asynchronously, and implement deduplication to handle the occasional duplicate delivery.
What events does Preserve send?
Preserve currently supports 5 event types:
- document.created — Fires when a file is uploaded or created in Preserve.
- document.updated — Fires when a document's name or description is changed.
- certificate.issued — Fires when a user earns a certificate, e.g. after completing a qualification.
- certificate.renewed — Fires when a user requalifies and their certificate gets a new expiration date.
- certificate.expired — Fires when a certificate passes its expiration date and is marked expired.
Note: More event types are planned. Check the Preserve roadmap for upcoming additions.
Does creating a folder trigger a webhook?
No. The document.created event only fires when a file is created. Creating a folder in Preserve does not trigger any webhook event.
Similarly, document.updated only fires when the document's name or description changes — not for internal bookkeeping or metadata changes that happen behind the scenes.
What data is included in a document event?
Both document.created and document.updated deliver a payload with the following fields:
- id — The document's unique ID in Preserve
- name — The document's name
- description — The document's description (or null if not set)
- organization_id / organization — Present for org-level documents
- site_id / site — Present for site-level documents
- url — A direct link to the document in Preserve (requires the user to be signed in with access)
Note: organization_id and site_id are mutually exclusive — a document belongs to either an org or a site, not both.
What data is included in a certificate event?
All three certificate events (certificate.issued, certificate.renewed, certificate.expired) include the following fields:
- id — The certificate's unique ID in Preserve
- certificate_number — A unique human-readable identifier (e.g. CERT-3F9A8C1B2D4E)
- title — The certificate title (e.g. "Confined Space Entry")
- status — Current status: active, expired, or revoked
- user_id / user / user_email — Identity of the certificate holder
- issue_date — The date the certificate was originally issued
- expires_at — Expiration date, or null if the certificate does not expire
- organization_id / organization — The owning organization
- url — A direct link to the certificate in Preserve (requires a signed-in admin with access)
How quickly will my endpoint receive events?
Deliveries are sent asynchronously and typically arrive within a few seconds of the triggering event. Preserve does not wait for your response before moving on — the POST is fired and your server handles it independently.
What happens if my server is slow or unavailable?
If your endpoint returns a non-2xx response, times out, or can't be reached, Preserve will retry the delivery with increasing delays for approximately 4 hours. After that window, the event is permanently dropped and will not be retried.
Important: Your endpoint must return a 2xx status code within roughly 10 seconds. If it takes longer, Preserve treats the delivery as failed and starts the retry process — even if your server eventually processes it. To avoid this, acknowledge the request immediately (return 200) and do any slow processing in a background job or queue.
Preserve does not follow redirects. Your webhook URL must be the final destination — a redirect response will be treated as a failure.
Can the same event be delivered more than once?
Yes. Under normal operation events arrive once, but retries mean the same event can occasionally be delivered more than once. Your handler should be built to handle this gracefully.
To deduplicate, use the X-Preserve-Delivery HTTP header included in every delivery. This is a unique ID for the delivery — and retries of the same event reuse the same delivery ID. Before processing an event, check whether you've already seen that ID. If you have, skip it.
Best practice: Store processed delivery IDs (e.g. in a database or cache) and check against them on every incoming request. This pattern is called idempotency and is standard practice for webhook consumers.
Can events arrive out of order?
Yes. In rare cases, a retried event can arrive after a newer event for the same resource. For example, a certificate.renewed event could theoretically arrive before a delayed certificate.issued event from earlier.
Do not rely on arrival order to determine the sequence of events. Instead, use the dates and timestamps inside the payload itself (such as issue_date or expires_at) to understand what state the record is actually in.
Do all my registered webhooks receive every event?
Yes. Every registered webhook receives every event type. There is no way to subscribe a specific webhook to only certain event types at registration time.
If your integration only cares about certain events, filter by the event field in the JSON payload on your end and ignore the ones you don't need.
What does the HTTP request look like when it arrives?
Every delivery is an HTTP POST request with the header Content-Type: application/json. The body is a JSON object that always follows the same structure:
- hook.name — The name you gave this webhook at registration. Useful if you have multiple webhooks posting to the same endpoint.
- event — The event type that fired (e.g. document.created, certificate.expired). Use this to branch your logic.
- payload — The event-specific data. The fields here differ depending on the event type.
In addition, every request includes the HTTP header X-Preserve-Delivery, which is a unique ID for this delivery. Use it to deduplicate retries.
Can I see a full example of a delivered payload?
Here is what a certificate.issued delivery looks like:
hook.name: "HR System Integration" event: "certificate.issued" payload: id: 91 certificate_number: "CERT-3F9A8C1B2D4E" title: "Confined Space Entry" status: "active" user_id: 17 user: "Dana Rivera" user_email: "dana.rivera@acme.example.com" issue_date: "2026-06-01" expires_at: "2027-06-01" organization_id: 7 organization: "Acme Energy" url: "https://app.example.com/learn/admin/certificates/91"
Your server should respond with HTTP 200 OK as quickly as possible.
Are webhook payloads signed so I can verify they're from Preserve?
Not yet. Preserve does not currently sign webhook payloads with a cryptographic signature. This means you cannot verify the source of a request by inspecting the payload alone.
Until signing is available, the recommended approach is to use a secret token in your endpoint URL. Generate a long random string and include it as part of your webhook path (e.g. yourserver.com/hooks/preserve/3f9a8c7b2d4e), then register that full URL in Preserve. On your server, check the token on every incoming request and reject anything that doesn't match. Keep this token secret and treat it like a password.
Why is HTTPS required?
Because payloads are not cryptographically signed, HTTPS is the only in-transit protection for your data. Without it, someone intercepting network traffic could read or tamper with webhook payloads before they reach your server.
Preserve enforces HTTPS at registration — you cannot register a plain HTTP URL. Make sure your server has a valid TLS/SSL certificate.
Should I trust the payload data directly for sensitive operations?
For most use cases, the data in the payload is reliable. However, for sensitive operations — granting access, triggering billing changes, updating compliance records — best practice is to treat the webhook as a notification, not a source of truth. Use the id or url in the payload to look the record up directly via the Preserve API and act on that confirmed data instead.
How do I test webhooks on my local machine during development?
Preserve needs to reach a publicly accessible HTTPS URL — your localhost address won't work directly. Tunneling tools solve this by creating a public URL that forwards traffic to your local machine.
- ngrok — the most popular option. Run ngrok http 3000 and you'll get a public HTTPS URL you can paste into Preserve.
- Webhook inspection services — tools like Webhook.site or RequestBin let you capture and inspect payloads without writing any code, useful for quickly verifying payload shape before you build your handler.
Should I use webhooks as my system of record?
No. Webhooks are designed as real-time notifications, not a guaranteed event log. If your endpoint stays down past the 4-hour retry window, those events are lost permanently. The same event can occasionally be delivered more than once. Events can occasionally arrive out of order.
For reporting, auditing, or any process where missing an event has serious consequences, use the Preserve API to query the state of records directly rather than relying solely on webhook delivery.
A good pattern is: webhook arrives → your system re-fetches the record via API → your system updates itself. This way, even a missed webhook doesn't leave your data permanently out of sync.
Still have questions? Visit the full Preserve Webhooks developer documentation at preserve.stg.tectrapro.com/docs/api/webhooks or contact your account team.