Webhook events
Delivery
Reply 2xx as soon as the event is stored, then do your processing afterwards. A slow handler
becomes a timeout, and a timeout becomes a retry.
Verify the signature
Your secret is the Webhook signing secret that the panel shows once, the first time you save your webhook. See Set your webhook.
Verify against the raw bytes, before JSON parsing. Re-serialising changes key order and
whitespace and the signature will never match. Use compare_digest, not ==.
If your endpoint needs its own authentication
Some partners cannot accept an unauthenticated POST. A bank’s gateway or WAF may require a bearer
token or an API key before the request reaches the application at all. Our signature proves the
payload came from us, but it does not satisfy your infrastructure’s rule.
You can give us a header set, and we attach it to every delivery, verbatim:
A plain bearer token works. It is just an Authorization header like any other.
The webhook URL, these headers and your signing secret all live in your panel. Only the return URL comes to us, because it decides where a real customer’s browser is sent mid-signup.
The signature is still what proves authenticity. Your headers get the request past your own
gate; X-ParsChat-Signature tells you the body is genuinely ours. Verify the signature even
when your own auth passed. A caller who learned your token could otherwise post you fabricated
events.
Not covered: mTLS. If your infrastructure mandates client certificates, tell us before you integrate. It requires a build on our side rather than a configuration change.
A complete receiver
X-ParsChat-Delivery is stable across retries of the same event. Store it and ignore a
delivery id you have already processed. That keeps you correct when a 2xx of ours is lost in
transit and we retry an event you did handle.
Send yourself a test event
Before you wait for real traffic, make us deliver to you on demand:
It is a real delivery, not a simulation: signed with your secret, carrying your custom headers, and retried on the same schedule as any other event. So it exercises your signature verification and your firewall rules, not merely whether your host resolves.
Its data carries "test": true so your handler can discard it instead of creating a conversation
from it.
The call returns 202 as soon as the event is queued — that is not proof your endpoint accepted
it. Look the delivery_id up in the delivery log below to see what you actually answered.
A 422 means no webhook URL is configured for your account yet. Set one in your ParsChat panel
first (Profile → Access keys & webhook).
Retries
Anything other than a 2xx, including a timeout, is retried with backoff: 6 attempts over about
an hour.
Then the delivery is dead-lettered and visible in the panel, so you can see what failed instead of discovering a silent gap.
Do not rely on retries as your only safety net. After the last attempt the event is not redelivered automatically. Use the usage API to reconcile a gap.
The delivery log
When an event you expected never arrived, this answers did you actually send it?
delivery_id is the same value that arrives in the event body, so you can match a log entry to
something you did or did not receive.
last_status_code and last_error are the useful pair when debugging: a 502 with
next_attempt_at set is transient and self-correcting, while six 500s ending in
dead_lettered is a gap you need to reconcile yourself.
Message bodies are not included. This is a delivery log, not a second way to read your conversations — those are governed by the conversations and usage endpoints, which apply their own rules.
Check your configuration
Read-only, so you can verify your configuration from code without being able to change it. The secret is never returned, only whether one is set.
The envelope
Every event has the same outer shape:
schema_version is semver. Fields are added without a major bump, so ignore ones you do not
recognise. Nothing is removed or repurposed without a new major version and notice.
Event types
message.generating and is_typing are two different facts. The first is the AI composing,
the second is a human operator typing. You may want to render them differently.
Full payloads for every event: Webhook event reference.
Message types
Attachments arrive as ordinary message events with a non-text type, never as a separate event:
text · voice · image · video · multimedia · document · poll · form · template
Four fields worth explaining
external_id: your idempotency key
If you send a message with an external_id you have used before, we do not create a duplicate.
Use it to make retries safe.
reply_to_id: threaded replies
The ParsChat message_id this message replies to, so you can render the quoted message.
null when the message is not a reply.
markdown_content: rich formatting
Set when the content has formatting. text stays the plain rendering, so you can use both:
markdown in a rich chat view, plain text in a notification or SMS fallback. Either may be
null, so never assume both are present.
form_data: structured form answers
A list of {type, situation, key, value}, present when type is form.
Forms are not Instagram-only. A customer connecting through the widget produces the same shape.