Requirements
- A HIPAA-compliant workspace — Enterprise plan with a signed BAA. See HIPAA compliance.
- An HTTPS endpoint that accepts
POSTrequests. Plainhttp://URLs are rejected. - Permission to edit workspace settings. Workspace Owners have this by default; it can be granted to other members through a custom role.
Configure your endpoint
1
Open the HIPAA settings
Go to Settings → HIPAA in your dashboard and find the Webhook configuration card.The HIPAA settings page only appears on workspaces where HIPAA compliance is enabled.
2
Save your endpoint URL
Enter your endpoint in the Webhook endpoint field and click Save.
3
Copy the signing secret
Saving a URL for the first time generates a signing secret — a 64-character hex string — and shows it in a one-time modal.
4
Verify the endpoint
Click Verify endpoint. Your endpoint must pass this check before any conversation is delivered — see Endpoint verification below.
Verify endpoint stays disabled until there is a saved URL and a signing secret, and while the URL field has unsaved edits. Save your changes first, then verify.
Endpoint verification
Verification proves your endpoint is safe to receive PHI. Clicking Verify endpoint sends twoPOST requests in sequence to the same URL:
Both must hold for verification to pass.
The second probe is the point of the design. An endpoint that returns
200 to anything would look perfectly healthy while accepting forged patient data from anyone who guessed its URL. Requiring a rejection proves your receiver actually checks the signature — and it only asks for behaviour you need in production anyway, so there is no verification-only code to write and later remove.
Both probes carry
"test": true, which real deliveries never do. Acknowledge them and do not store them.They also deliberately use different delivery_id values, so a correctly idempotent receiver does not discard the second one as a duplicate.Verification failure reasons
Until verification passes, the card shows Endpoint not verified and nothing is delivered. Conversations that reach their retention threshold in the meantime are held and retried — but they spend retry attempts while they wait, so verify early.
The delivery request
Each conversation is sent as its ownPOST request with a JSON body.
Headers
Payload
Envelope fields
Conversation fields
Theconversation object uses snake_case keys.
Source values
source records the channel the conversation arrived through. On a HIPAA-compliant workspace you can expect:
Treat the list as open-ended: new channels add new values, so route with a fallback rather than an exhaustive match.
Message shape
messages holds the conversation history, oldest first.
Message fields
These can appear on a message of any role. Every field exceptrole is optional, so check for presence rather than assuming a fixed shape.
Assistant messages may also carry:
User messages may also carry:
source means two different things at two different levels. conversation.source is the channel (Widget or Iframe, API, …); a message’s source is where that answer came from (llm, qna, …). They share a name but not a value set.Content parts
content is a string on plain text messages and an array on tool-call and tool-result messages. Calling something like String(content) breaks on any conversation where the agent used an action.
input and output are action-specific and, for custom actions and forms, contain whatever your own integration returned. Treat them as opaque JSON unless you know the action.
Attachments
Parsing notes
createdAtis historically inconsistent. Conversations backfilled through the API may use a space instead ofT, or omit the minutes in the UTC offset. Parse it defensively.- Ignore fields you do not recognise rather than treating them as an error, so a future addition does not break your receiver.
Example
Verify the signature
Every request carries an HMAC-SHA256 signature. Recompute it and reject anything that does not match — otherwise anyone who learns your endpoint URL can post fabricated patient data to it. The signature is computed over the timestamp, a literal., and the raw request body:
v1= prefix identifies the scheme, so it can change in future without breaking existing receivers. Compare the full v1=… string, prefix included.
Your receiver should, in this order:
- Reject missing headers with a 4xx, rather than letting them fall through and look like a signature mismatch.
- Check the timestamp is recent — within about 5 minutes. The timestamp is part of the signed input, but only checking it makes replay protection real; without this check, a captured request stays valid forever.
- Take the timestamp from the header, not from your own clock. A locally generated one will never match.
- Recompute the HMAC and compare in constant time.
- Only then parse and trust the body. Never act on payload contents before the signature checks out.
Why compare in constant time?
Why compare in constant time?
A plain
=== (or ==) on strings stops at the first byte that differs, so how long the comparison takes leaks how many leading bytes matched. In principle an attacker can send many requests, measure the response times, and recover a valid signature one byte at a time — turning an infeasible search into roughly a thousand guesses.In practice, network jitter dwarfs the timing difference, so this is hardening rather than a likely attack path. But it costs nothing: use crypto.timingSafeEqual in Node, hmac.compare_digest in Python, or your language’s equivalent.If your platform has no constant-time primitive, hash both values again with a random per-request key and compare those results normally. An attacker cannot steer timing against a key they do not know.Responding
- Return any 2xx status to acknowledge the delivery. Any other status is treated as a failure and retried.
- Chatbase closes the connection after 10 seconds. Acknowledge first and process asynchronously — slow processing turns into timeouts, which turn into duplicate deliveries.
- Your response body is ignored.
Retries
If a delivery fails, Chatbase retries it on a widening schedule:
That is 6 attempts over roughly 22 hours. Each wait is jittered by ±20%, so a batch of deliveries that fails together — one outage, one bad deploy — does not come back as a synchronised burst.
A failure is any non-2xx response, a timeout, or a connection, DNS, or TLS error.
Attempts are also consumed while the webhook itself is not ready to receive: a delivery waiting on an unverified endpoint, or on a missing signing secret, uses up an attempt each time it is tried. Verifying your endpoint before conversations start reaching their retention thresholds is what keeps the ladder available for real failures.
Idempotency
Delivery is at-least-once, so your receiver must tolerate duplicates. Retries reuse the sameX-Chatbase-Delivery-Id. A delivery can also arrive twice legitimately — if your endpoint processed a request but the acknowledgement was lost on the way back, Chatbase never recorded the success and will send it again.
Deduplicate on delivery_id, or on conversation.id, which appears in at most one conversation’s worth of deliveries. Record the ID in the same transaction that stores the conversation so a crash between the two cannot lose or double-count it.
Changing your endpoint or secret
Two rules explain the whole table:
- The current secret is used at the moment of each attempt. Rotating is a security action — if you rotate because you believe the old secret leaked, the very next delivery must not still be signed with it. So rotation takes effect immediately, including for conversations already waiting.
- Verification attests to one specific endpoint. A different URL has not proved anything, so changing it clears verification and you must verify again.
Troubleshooting
Verification fails with accepted_invalid_signature
Verification fails with accepted_invalid_signature
Your endpoint returned 2xx to a request with a deliberately invalid signature. Either it is not checking the signature at all, or it returns a response before the check runs — a common shape is an early
return res.status(200) for health checks or an OPTIONS/POST handler that acknowledges first and validates later.Check that an invalid signature produces a non-2xx status, and that the check happens before anything else responds.Verification fails with rejected_signed_request
Verification fails with rejected_signed_request
Your endpoint returned a non-2xx to a correctly signed request. The usual cause is raw-body handling — see the warning in Verify the signature.Also confirm the endpoint is publicly reachable over HTTPS and is not behind authentication, an IP allowlist, or a WAF rule that blocks unknown callers.
Verification fails with redirect
Verification fails with redirect
Your endpoint answered with a
3xx instead of handling the request. Redirects are not followed, so the URL you save has to be the one that actually serves the webhook.Most often this is a trailing slash or www canonicalisation rather than anything you configured deliberately — see the full list of causes under Configure your endpoint. Sending a POST to your saved URL with curl -i and checking for a Location header is the quickest way to confirm it.The signature never matches
The signature never matches
In order of likelihood:
- The body was re-serialized. A JSON body parser ran before you computed the HMAC. Use the raw bytes.
- The timestamp came from the wrong place. It must be read from the
X-Chatbase-Timestampheader, not generated locally. - The signed input is malformed. It is
timestamp + "." + rawBody— a literal period between the two, and nothing else. - The secret has stray whitespace. A trailing newline picked up when pasting into an environment file or secrets manager will change every digest.
- The
v1=prefix was dropped. Compare the whole header value, includingv1=.
Only one request arrived during verification
Only one request arrived during verification
The first probe failed, so the second was never sent. Fix the failure reason shown in the dashboard and verify again.
A conversation arrived twice
A conversation arrived twice
Expected behaviour — delivery is at-least-once. Deduplicate on
delivery_id. See Idempotency.A payload arrived with test: true
A payload arrived with test: true
That is a verification probe, not a real conversation. Acknowledge it with a 2xx and do not store it, or you will save a fake conversation into your records.
Nothing is arriving
Nothing is arriving
Check, in order:
- The Webhook configuration card shows Endpoint verified. If it shows Endpoint not verified, nothing is being delivered.
- Conversations have actually reached a retention threshold — a conversation is only delivered when it is deleted, so nothing arrives for conversations that are still active or idle for less than 24 hours.
- Your endpoint URL is still correct, and saving it did not silently reset verification.
Notes and limits
- One webhook per workspace, covering every AI agent in it. Use
conversation.chatbot_idto tell them apart. - One conversation per request. Deliveries are never batched, so each conversation gets its own status code and can succeed or fail independently.
- HTTPS only.
- Deletion is never delayed. Conversations are deleted on schedule whether or not delivery succeeds, so your endpoint being down does not extend the retention window.
- Chatbase does not log the conversation content of a delivery. Delivery attempts are recorded for support and audit purposes with only the outcome, response status, and error reason. Your endpoint’s hostname is recorded in audit events; the full URL and the signing secret never are.
HIPAA compliance overview
Retention rules, redaction, disabled features, and the shared responsibility model for HIPAA-compliant workspaces.
