What a webhook does
A webhook sends a message from Klovis to another system the moment something happens in your workspace. You give Klovis an address (an endpoint URL), pick the events you care about, and Klovis posts a JSON payload to that address each time one of them happens.
Most people point a webhook at Zapier, Make or n8n, and use it to create or update a contact in their CRM. You can also point it at your own server.
Traffic only goes one way: Klovis sends to your endpoint. Nothing is received by Klovis at that address.
Create a webhook
- Go to Settings > Webhooks.
- Click New webhook.
- Fill in:
- Name, so you can tell your webhooks apart, for example "CRM sync".
- Endpoint URL, the address to send to. Use an HTTPS address.
- Events, one or more of the events below.
- Retries, how many times Klovis tries again if your endpoint does not answer successfully (0 to 10).
- Timeout (seconds), how long Klovis waits for your endpoint to answer (1 to 120).
- Active, which you can turn off to pause a webhook without deleting it.
- Click Create webhook.
Klovis then shows the webhook's signing secret, once. Copy it now if you need it (see "Checking a request came from Klovis" below). You will not be able to see it again.
The events
Prospect accepted a connection request. Fires once per person when they accept a LinkedIn connection request from one of your sending accounts, however Klovis spots the acceptance.
Prospect replied to you. Fires when a message from the person arrives, never when you send one. Every message fires its own event, not just the first, so a person who writes three times fires three events.
A tag was added to a person. Fires once for each tag added to a person, whether a teammate added it, a workflow did, or it came from a bulk action on a list. Choose which tags count: click the underlined word in the sentence "When any tag was added to a person" and pick one or more tags, or choose Any tag. Adding three tags at once fires three events. A tag the person already had fires nothing. Tags added by an import do not fire this event.
A few things to know about replies:
- A reply only fires for a person who is in a campaign that is running or paused. A reply from someone you messaged by hand, or whose campaign has already ended, sends nothing.
- If a campaign is set to keep sending through out-of-office replies, those auto-replies do not fire an event.
Deliveries can arrive up to a minute after the event.
Test it
After saving, click Test on the webhook's row. Klovis sends a complete sample payload straight away and tells you what your endpoint answered. The sample has every field filled in, so Zapier and similar tools can see every field you might want to map. Test payloads carry "is_test": true; real ones carry false, so you can filter tests out.
To see what was sent recently and how your endpoint answered, open the webhook's ... menu and click Logs.
The payload
Every event sends the same shape. Every key is always there, even when it has no value, in which case it is null. This matters for tools like Zapier, which only let you map fields that appeared in the sample they captured.
{
"event": "prospect.replied",
"event_key": "reply:abc123",
"occurred_at": "2026-08-04T11:36:41+00:00",
"is_test": false,
"channel": "linkedin",
"contact": { ... },
"sending_account": { ... },
"campaign": { ... },
"connection": null,
"conversation": { ... },
"tagged": null,
"history": { ... }
}
- event is
prospect.accepted,prospect.repliedorprospect.tagged. - event_key is the same every time the same thing is sent, so you can use it to ignore a repeat.
- occurred_at and every other time are in UTC, in ISO 8601 format.
- channel is the channel the event happened on, such as
linkedin. It isnullfor a tag event. - Exactly one of connection, conversation and tagged is filled in, depending on the event. The other two are
null.
contact
The person the event is about. This is what becomes the contact in your CRM.
"contact": {
"id": "9c81f3a2-...",
"klovis_url": "https://app.kloviscrm.com/records/9c81f3a2-...",
"full_name": "Sam Taylor",
"first_name": "Sam",
"last_name": "Taylor",
"email": "9c81f3a2-...@unknown.com",
"email_is_placeholder": true,
"email_alt": null,
"phone": null,
"phone_alt": null,
"job_title": "Head of Operations",
"company_name": "Example Logistics",
"company_linkedin_url": "https://linkedin.com/company/example-logistics",
"location": "Toronto, Canada",
"website": null,
"industry": "Logistics",
"employee_count": "201-500",
"linkedin_url": "https://linkedin.com/in/sam-taylor-example",
"linkedin_handle": "sam-taylor-example",
"whatsapp_id": null,
"telegram_id": null,
"instagram_id": null,
"prospect_status": "connected",
"lead_stage": "connected",
"tags": ["canada", "logistics"],
"owner_name": "Your teammate"
}
About the email field. Most CRMs need an email address to create a contact, and many LinkedIn contacts have none. So email is always filled in. When Klovis has no real address for the person, it sends a placeholder ending in @unknown.com and sets email_is_placeholder to true. Map the one email field, and branch on the flag. Klovis never sends to these placeholders itself, but your CRM does not know that, so keep placeholder addresses out of any email sequence on the CRM side. If a real address turns up later, the next event carries it with the flag set to false.
The contact's details are read when the webhook is delivered, so they are up to date at that moment.
sending_account
Your side of the event: the account the person accepted or messaged.
"sending_account": {
"id": "4034583e-...",
"label": "Alex Morgan",
"name": "Alex Morgan",
"email": "alex@example.com",
"provider": "LINKEDIN"
}
email can be null, so do not key anything on it. Use id if you need to tell accounts apart. This block is null on tag events.
campaign
The campaign the person was in when the event happened.
"campaign": {
"id": "82c396cc-...",
"name": "Autumn outreach",
"enrollment_status": "paused",
"enrolled_at": "2026-07-28T08:15:00+00:00",
"step_number": 2,
"exit_reason": null
}
enrollment_status is the person's state in the campaign (active, paused, finished or removed), not whether the campaign itself is running. The whole block is null when there is no campaign behind the event, for example a connection accepted outside any campaign, and on tag events. Allow for that in your mapping.
connection (accepted events)
"connection": { "accepted_at": "2026-08-04T09:14:22+00:00" }
conversation (replied events)
"conversation": {
"received_at": "2026-08-04T11:36:41+00:00",
"is_first": true,
"sentiment": "positive",
"is_automated": false
}
- is_first is
trueonly for the person's first reply. Filter on it if something should happen once per person, such as creating a deal. - sentiment is
positive,negative,auto_replyorneutral. - is_automated is
truefor an automatic reply such as an out-of-office.
tagged (tag events)
"tagged": {
"tag": "hot-lead",
"tags": ["canada", "logistics", "hot-lead"]
}
tag is the tag that was just added. tags is every tag the person had right after it was added.
history
A short history of the person, on every event.
"history": {
"campaigns": [
{
"id": "82c396cc-...",
"name": "Autumn outreach",
"status": "paused",
"enrolled_at": "2026-07-28T08:15:00+00:00",
"exited_at": null,
"exit_reason": null,
"step_number": 2
}
],
"last_message": {
"direction": "inbound",
"channel": "linkedin",
"sending_account_id": "4034583e-...",
"sent_at": "2026-08-04T11:36:41+00:00",
"text": "Thanks, happy to talk next week.",
"thread_id": "5b0e..."
}
}
- campaigns lists every campaign the person is in or has been in, newest first.
statusis the person's state in that campaign. It is an empty list if they have never been in one. - last_message depends on the event. On a reply, it is the reply that fired the event. On a tag event, it is the most recent message with the person in either direction, on any channel including email. On an accepted event it is
null, because accepting a connection carries no message. Long messages are cut at 2,000 characters. Website chat conversations are not included.
Checking a request came from Klovis
Every request carries these headers:
Content-Type: application/json
X-Event-Type: prospect.accepted
X-Event-Key: accept:9c81f3a2-...
X-Signature: sha256=<signature>
X-Signature is an HMAC SHA-256 of the raw request body, made with the webhook's signing secret. If you own the code that receives the webhook, compute the same signature and reject any request where the two do not match.
You do not need the secret for Zapier or Make. Their catch hooks accept any request and do not check signatures.
Related articles
- "Campaign states explained", for what each campaign status means.
