Skip to main content
HocDoc can call your server when something happens to an offer. You add an endpoint (an HTTPS address) in the app, choose the events you want, and HocDoc sends a signed 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 ping event to the endpoint, so you can check the address and your signature check.

Requirements for the address

  • https only, 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 3xx response counts as a failure.
  • Hosts on hocdoc.com, vercel.app and supabase.co are 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.offer is read shortly after the event, when the webhook is prepared. It can already reflect a later change. For offer.sent followed quickly by offer.accepted, both bodies may show status: accepted. Rely on type and data.event for what happened.
  • data.event.type is the type of the timeline entry. For offer.first_viewed it is viewed.
  • data.event.data depends on the type:
  • For ping, data is 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.
  1. Take the secret of the endpoint, remove the prefix whsec_, and base64-decode the rest. These bytes are the key.
  2. 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.
  3. Compute the HMAC-SHA256 of the signed content with the key and base64-encode it.
  4. webhook-signature contains one or more entries separated by spaces. Accept the request if any entry with the version v1 equals your result. Compare in constant time.
  5. Reject requests whose webhook-timestamp is more than 5 minutes away from your clock. This limits replays.
The header is a list so that a secret can be replaced later without breaking receivers. Today it has one entry.

Node.js

With Express, read the raw body for this route (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. A ping as HocDoc sends it: The reference vector of the Standard Webhooks specification:

Respond

Answer with any 2xx 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) or created_at if you need the order.
  • Events can arrive more than once. Store the webhook-id values 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.