POST request with a JSON body for each event.
Webhooks use the same objects as the API: the body contains the offer as returned by
GET /api/v1/offers/{id} and the timeline entry as returned by GET /api/v1/offers/{id}/events. The machine-readable
contract is the webhooks section of the OpenAPI description at https://app.hocdoc.com/api/v1/openapi.json.
Set up an endpoint
In the app: Einstellungen, then the Integrationen tab, card Webhooks, button Endpunkt hinzufügen. Owners and admins can manage endpoints. A workspace can have up to 5 endpoints. One account can set up endpoints in up to 20 workspaces.- Enter the address and choose the events.
- The secret (
whsec_…) is shown exactly once. Store it safely, you need it to verify signatures. If you lose it, generate a new one (“Geheimnis neu erzeugen”). The old secret stops working immediately. - “Test senden” sends a
pingevent to the endpoint, so you can check the address and your signature check.
Requirements for the address
httpsonly, on the default port 443.- A public host name. IP addresses, credentials in the address (
user:password@) and internal names are rejected. - The name must resolve to public addresses only.
- Redirects are not followed. A
3xxresponse counts as a failure. - Hosts on
hocdoc.com,vercel.appandsupabase.coare not accepted. The same goes for a custom domain that a workspace uses for its offer pages.
Events
offer.expired is detected every 15 minutes and sent out about two minutes later, so it arrives up to about 20
minutes after expires_at.
There is no offer.declined event: recipients cannot decline an offer in the product today.
New event types may be added. Ignore types you do not know, and answer them with 2xx.
Request
These three headers,
Content-Type and User-Agent are the contract. HocDoc adds no tracing or other headers of its
own.
Body
Things to know:
-
data.offeris read shortly after the event, when the webhook is prepared. It can already reflect a later change. Foroffer.sentfollowed quickly byoffer.accepted, both bodies may showstatus: accepted. Rely ontypeanddata.eventfor what happened. -
data.event.typeis the type of the timeline entry. Foroffer.first_viewedit isviewed. -
data.event.datadepends on the type: -
For
ping,datais an empty object. - New fields may be added at any time. Ignore fields you do not know.
- The body is at most 64 KiB.
What is never sent
The token of the public link, acceptance codes, IP addresses and browser identifiers of recipients, the content of an offer, the message to the recipient, signatures, internal IDs, and anything about other workspaces.Verify the signature
Always verify the signature before you trust a request. HocDoc signs following the Standard Webhooks specification, so you can use one of its libraries or the few lines below.- Take the secret of the endpoint, remove the prefix
whsec_, and base64-decode the rest. These bytes are the key. - Build the signed content:
<webhook-id>.<webhook-timestamp>.<raw body>. Use the raw bytes of the request body, exactly as received. Do not parse and re-serialize the JSON first. - Compute the HMAC-SHA256 of the signed content with the key and base64-encode it.
webhook-signaturecontains one or more entries separated by spaces. Accept the request if any entry with the versionv1equals your result. Compare in constant time.- Reject requests whose
webhook-timestampis more than 5 minutes away from your clock. This limits replays.
Node.js
express.raw({ type: "application/json" })) and parse the JSON only
after the check.
Python
Test vectors
Use these to check your implementation. The timestamps are in the past, so skip the tolerance check for them. Aping as HocDoc sends it:
The reference vector of the Standard Webhooks specification:
Respond
Answer with any2xx status within 10 seconds. The response body is ignored. Do slow work after you have answered.
Everything else counts as a failed attempt: any other status (including 3xx), no response within 10 seconds, a
connection or TLS error.
Retries
A failed delivery is retried with growing pauses, 8 attempts in total over about a day:
After the 8th failed attempt the delivery counts as failed. You can send it again from the delivery log in the app
(“Erneut senden”), up to 5 times. A resend carries the same
webhook-id and the same body.
Automatic pause
After 5 deliveries in a row that failed for good, the endpoint is paused automatically and its open deliveries are cancelled. The app shows “Automatisch pausiert”. Fix the receiver, then resume the endpoint in the app. Events that happen while an endpoint is paused are not delivered later. Use the API to catch up.Order and duplicates
- Order is not guaranteed. Events can arrive out of order, for example when one of them is retried. Use
data.event.sequence(the position in the timeline of the offer) orcreated_atif you need the order. - Events can arrive more than once. Store the
webhook-idvalues you have processed and ignore repeats. - Each endpoint receives each event independently. One failing endpoint does not hold back the others.
Limits
Deliveries above 60 per minute are not dropped, they wait. Each workspace has its own share: the volume of another
workspace does not delay your deliveries. Workspaces take turns, and the one that has waited longest goes first. A
workspace that sets up its first endpoint joins at the back of the line, never ahead of workspaces that are already
waiting.
This is not a hard guarantee on delivery time. Sending goes round-robin per workspace, so the time your workspace
waits grows at most with the number of workspaces that are waiting at the same moment. No workspace is skipped, but
when very many workspaces have events waiting at once, all of them wait longer.
The limit of 20 workspaces counts the workspaces in which your account created endpoints, paused ones included. If
you need webhooks in more workspaces, another owner or admin of that workspace can set them up. When you delete the
last endpoint of a workspace, events of that workspace that were still waiting to be sent out are dropped.
An endpoint that already has 1,000 open deliveries (waiting or being retried) does not get more queued. Further
deliveries to it are recorded as failed in the delivery log without an attempt (“Nicht eingereiht”) and can be sent
again from there. If 1,000 events of a workspace are waiting to be sent out at the same moment, further events are not
turned into webhooks. Neither limit is reached in normal use. Use the API to catch up.