Integrations

Webhooks

Contaktly can send every company it identifies on your website to a URL of yours, as signed JSON, so it lands in your CRM, a Zapier or Make scenario, or your own system. This page is for whoever builds the receiving end.

Delivery
An HTTPS POST with a JSON body
Signed
HMAC-SHA256 with your own secret
Retries
6 attempts over 36 hours
Cost
Included in every plan, uses no identifications

Set it up

  1. In Contaktly, open Settings, Integrations and find Any other tool. Paste the https address of your receiver and save it.
  2. Copy the signing secret that appears. It is shown once. Keep it where your receiver can read it, not in your code.
  3. Press Send an example. The card shows what your URL answered. The example is the company.test event with made-up data, in exactly the shape below.
  4. Switch on Send automatically. From then on every newly identified company is sent. You can also send a single company from Identify with Send to your URL.

When we send

EventSent when
company.identifiedA company visited your website and we identified it. Sent for every visit, so a company that comes back three times arrives three times, each time with its whole visit history so far.
company.testYou pressed Send an example. The same shape, with made-up data.

Automatic sends go out within about 15 minutes of the visit, once we have looked for the people at the company, which usually takes a minute, so the delivery carries them. Companies held because your workspace is over its monthly identification limit are not sent, and a later upgrade does not send them. Nothing else is filtered: a bounce arrives too, with visits.real set to false.

Automatic sends never verify an email address themselves, so they carry a person’s email only once someone has verified it on Identify. See emails below.

The request

Every delivery is one POST to your address with these headers and a JSON body.

HeaderMeaning
Content-TypeAlways application/json, UTF-8.
User-AgentContaktly-Webhook/1.
X-Contaktly-EventThe event, the same as event in the body: company.identified, or company.test for an example.
X-Contaktly-DeliveryOne id per delivery, the same on every retry of it. Keep the ones you have processed and skip a repeat.
X-Contaktly-Attempt1 on the first try, rising with each retry, 6 at most.
X-Contaktly-Signaturesha256= followed by the HMAC-SHA256 of the raw body, keyed with your signing secret, in lower-case hex.

A complete delivery, as your server receives it:

HTTP
POST /contaktly HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: Contaktly-Webhook/1
X-Contaktly-Event: company.identified
X-Contaktly-Delivery: 3f6c1e2a-8b4d-4c7e-9a1f-5d2b7c9e0a14
X-Contaktly-Attempt: 1
X-Contaktly-Signature: sha256=bb26f3c5298c01eb07206b6beb0da38873054086561cac86b8f074dd410c2a5f

{
  "version": 1,
  "event": "company.identified",
  "sentAt": "2026-09-23T09:14:07.000Z",
  "workspaceId": "9d1b7c3e-2f4a-4b8c-8e6d-1a5f3c7b9e20",
  "company": {
    "id": "00000000-0000-0000-0000-000000000000",
    "name": "Example Company",
    "domain": "example.com",
    "industry": "Software",
    "employees": "11-50 employees",
    "location": "Amsterdam, Netherlands",
    "linkedin": "https://www.linkedin.com/company/example"
  },
  "visits": {
    "count": 2,
    "first": "2026-09-20T07:14:07.000Z",
    "last": "2026-09-23T09:14:07.000Z",
    "totalSeconds": 95,
    "real": true,
    "intent": "warm",
    "pages": [
      "/pricing",
      "/"
    ]
  },
  "people": [
    {
      "id": "00000000-0000-0000-0000-000000000002",
      "name": "Ben Sample",
      "firstName": "Ben",
      "lastName": "Sample",
      "title": "Chief Executive Officer",
      "email": null,
      "emailStatus": "unverified",
      "linkedin": "https://www.linkedin.com/in/ben-sample",
      "phone": null
    },
    {
      "id": "00000000-0000-0000-0000-000000000001",
      "name": "Ann Example",
      "firstName": "Ann",
      "lastName": "Example",
      "title": "Head of Marketing",
      "email": "ann@example.com",
      "emailStatus": "valid",
      "linkedin": "https://www.linkedin.com/in/ann-example",
      "phone": null
    }
  ],
  "link": "https://app.contaktly.com/identify?company=example.com"
}

Shown indented for reading. The real body is compact JSON on one line, and the signature covers the exact bytes you receive.

To try your receiver before connecting it, this sends the same delivery exactly as we would, signed with the example secret further down:

curl
curl -X POST 'https://hooks.example.com/contaktly' \
  -H 'Content-Type: application/json' \
  -H 'User-Agent: Contaktly-Webhook/1' \
  -H 'X-Contaktly-Event: company.identified' \
  -H 'X-Contaktly-Delivery: 3f6c1e2a-8b4d-4c7e-9a1f-5d2b7c9e0a14' \
  -H 'X-Contaktly-Attempt: 1' \
  -H 'X-Contaktly-Signature: sha256=bb26f3c5298c01eb07206b6beb0da38873054086561cac86b8f074dd410c2a5f' \
  --data-binary '{"version":1,"event":"company.identified","sentAt":"2026-09-23T09:14:07.000Z","workspaceId":"9d1b7c3e-2f4a-4b8c-8e6d-1a5f3c7b9e20","company":{"id":"00000000-0000-0000-0000-000000000000","name":"Example Company","domain":"example.com","industry":"Software","employees":"11-50 employees","location":"Amsterdam, Netherlands","linkedin":"https://www.linkedin.com/company/example"},"visits":{"count":2,"first":"2026-09-20T07:14:07.000Z","last":"2026-09-23T09:14:07.000Z","totalSeconds":95,"real":true,"intent":"warm","pages":["/pricing","/"]},"people":[{"id":"00000000-0000-0000-0000-000000000002","name":"Ben Sample","firstName":"Ben","lastName":"Sample","title":"Chief Executive Officer","email":null,"emailStatus":"unverified","linkedin":"https://www.linkedin.com/in/ben-sample","phone":null},{"id":"00000000-0000-0000-0000-000000000001","name":"Ann Example","firstName":"Ann","lastName":"Example","title":"Head of Marketing","email":"ann@example.com","emailStatus":"valid","linkedin":"https://www.linkedin.com/in/ann-example","phone":null}],"link":"https://app.contaktly.com/identify?company=example.com"}'

The payload

Emails are included only once verified

A delivery carries a person’s email only when we have verified it, and automatic sends do not verify addresses themselves: verifying is a deliberate step on Identify, where you choose the people you want. So most people in an automatic send arrive with email set to null and emailStatus set to unverified, with their name, title and LinkedIn profile. To send someone with their email, choose them on Identify, which verifies the address, and send the company from there with Send to your URL.

JSON
{
  "version": 1,
  "event": "company.identified",
  "sentAt": "2026-09-23T09:14:07.000Z",
  "workspaceId": "9d1b7c3e-2f4a-4b8c-8e6d-1a5f3c7b9e20",
  "company": {
    "id": "00000000-0000-0000-0000-000000000000",
    "name": "Example Company",
    "domain": "example.com",
    "industry": "Software",
    "employees": "11-50 employees",
    "location": "Amsterdam, Netherlands",
    "linkedin": "https://www.linkedin.com/company/example"
  },
  "visits": {
    "count": 2,
    "first": "2026-09-20T07:14:07.000Z",
    "last": "2026-09-23T09:14:07.000Z",
    "totalSeconds": 95,
    "real": true,
    "intent": "warm",
    "pages": [
      "/pricing",
      "/"
    ]
  },
  "people": [
    {
      "id": "00000000-0000-0000-0000-000000000002",
      "name": "Ben Sample",
      "firstName": "Ben",
      "lastName": "Sample",
      "title": "Chief Executive Officer",
      "email": null,
      "emailStatus": "unverified",
      "linkedin": "https://www.linkedin.com/in/ben-sample",
      "phone": null
    },
    {
      "id": "00000000-0000-0000-0000-000000000001",
      "name": "Ann Example",
      "firstName": "Ann",
      "lastName": "Example",
      "title": "Head of Marketing",
      "email": "ann@example.com",
      "emailStatus": "valid",
      "linkedin": "https://www.linkedin.com/in/ann-example",
      "phone": null
    }
  ],
  "link": "https://app.contaktly.com/identify?company=example.com"
}
FieldTypeMeaning
versionnumberAlways 1 for this shape. Within version 1 we only ever add fields.
eventstringcompany.identified for a real company. company.test for the example the Send an example button sends.
sentAtstringWhen this attempt was sent, ISO 8601 in UTC. It is inside the signed body, so you can refuse a delivery that is too old.
workspaceIdstringYour Contaktly workspace. Useful when one receiver serves more than one workspace.
companyobjectThe company that visited.
company.idstringThe id of the company’s most recent visit. Each visit has its own, so use domain to recognise a company you already hold.
company.namestringThe company name, or its domain when we have no name.
company.domainstring or nullThe company’s website domain, for example example.com. The best key for matching a company in your system.
company.industrystring or nullThe industry as our data source names it.
company.employeesstring or nullA size band as text, for example 11-50 employees.
company.locationstring or nullCity and country, where known.
company.linkedinstring or nullThe company’s LinkedIn page.
visitsobjectEvery visit this company has made so far, added up.
visits.countnumberHow many visits we have identified from this company.
visits.firststring or nullWhen the first visit was identified, ISO 8601 in UTC.
visits.laststring or nullWhen the most recent visit was identified, ISO 8601 in UTC.
visits.totalSecondsnumberTime on your site across all visits, in seconds.
visits.realbooleanTrue when the visits add up to at least ten seconds or two pages. False is usually a bounce.
visits.intentstringhot, warm or cool, from the pages they saw, the time they spent and how often they came back.
visits.pagesarray of stringsThe pages they viewed, as paths, most viewed first.
peoplearrayThe decision-makers we found there, most senior first. Automatic sends carry up to five; a send from Identify carries the people you picked. Empty when we found nobody who fits your targeting.
people[].idstringThe person’s id in your workspace. The same person keeps the same id.
people[].namestringFull name.
people[].firstNamestring or nullFirst name.
people[].lastNamestring or nullLast name.
people[].titlestring or nullJob title, as it reads on their profile.
people[].emailstring or nullA verified address, and only that. Anything we could not verify is null, never a guess.
people[].emailStatusstring or nullWhy email is what it is. See the list below.
people[].linkedinstring or nullTheir LinkedIn profile.
people[].phonestring or nullA phone number Identify looked up for this person, on workspaces where that is switched on. Null everywhere else.
linkstringOpens this company on Identify in Contaktly.

emailStatus

  • validVerified. The address is in email.
  • unverifiedWe have not checked an address for this person yet. Choosing them on Identify checks it; a send after that carries it.
  • checkingA check is running now.
  • catch_allTheir mail server accepts every address, so none can be confirmed. email stays null.
  • invalid, unknown, no_emailNo usable address was found.
  • nullNothing has been looked up for this person.

Checking the signature

Check every delivery before you trust it. X-Contaktly-Signature is sha256= followed by the HMAC-SHA256 of the raw request body, keyed with your signing secret, in hex. Compute the same over the bytes you received and compare the two in constant time.

Read the raw body before any JSON parser touches it. Parsing and writing it out again changes spacing or key order, and the signature no longer matches. Every example below reads the raw bytes.

The check
const crypto = require('crypto')

function isFromContaktly(rawBody, signature, secret) {
  const expected = Buffer.from('sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'))
  const received = Buffer.from(signature || '')
  return expected.length === received.length && crypto.timingSafeEqual(expected, received)
}
A receiver that uses it
const express = require('express')
const app = express()

// express.raw keeps the body as the exact bytes we sent.
app.post('/contaktly', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.get('X-Contaktly-Signature')
  if (!isFromContaktly(req.body, signature, process.env.CONTAKTLY_WEBHOOK_SECRET)) {
    return res.sendStatus(401)
  }
  const delivery = JSON.parse(req.body)
  // Skip it if you have seen req.get('X-Contaktly-Delivery') before,
  // then store delivery.company and delivery.people.
  res.sendStatus(200)
})

app.listen(3000)

Check your code against a known answer

Before you connect, run your check with these three values. It must say yes, and no once you change a single character of the body.

Secret
whsec_5f2a9c0e7b1d4e8a9c3f6b2d7e1a4c8f0b3d6e9a2c5f8b1d
Body, exactly these bytes
{"version":1,"event":"company.identified","sentAt":"2026-09-23T09:14:07.000Z","workspaceId":"9d1b7c3e-2f4a-4b8c-8e6d-1a5f3c7b9e20","company":{"id":"00000000-0000-0000-0000-000000000000","name":"Example Company","domain":"example.com","industry":"Software","employees":"11-50 employees","location":"Amsterdam, Netherlands","linkedin":"https://www.linkedin.com/company/example"},"visits":{"count":2,"first":"2026-09-20T07:14:07.000Z","last":"2026-09-23T09:14:07.000Z","totalSeconds":95,"real":true,"intent":"warm","pages":["/pricing","/"]},"people":[{"id":"00000000-0000-0000-0000-000000000002","name":"Ben Sample","firstName":"Ben","lastName":"Sample","title":"Chief Executive Officer","email":null,"emailStatus":"unverified","linkedin":"https://www.linkedin.com/in/ben-sample","phone":null},{"id":"00000000-0000-0000-0000-000000000001","name":"Ann Example","firstName":"Ann","lastName":"Example","title":"Head of Marketing","email":"ann@example.com","emailStatus":"valid","linkedin":"https://www.linkedin.com/in/ann-example","phone":null}],"link":"https://app.contaktly.com/identify?company=example.com"}
X-Contaktly-Signature
sha256=bb26f3c5298c01eb07206b6beb0da38873054086561cac86b8f074dd410c2a5f

Answering and retries

Answer with any 2xx status within 10 seconds and the delivery is done. What you put in the response body is ignored. If your work takes longer, store the body, answer 200 straight away and do the work after.

Anything else counts as a failure: another status, a redirect, a timeout or no connection at all. We do not follow redirects, so give us the final address.

An automatic send that fails is tried again after 15 minutes, 1 hour, 3 hours, 8 hours and 24 hours: 6 attempts over about 36 hours, then it stops. Every attempt of one delivery has the same X-Contaktly-Delivery id and a rising X-Contaktly-Attempt. Each attempt is built fresh, so a retry can carry people we found in the meantime, and it has a new sentAt.

A retry goes to the address saved at that moment, so fixing your address in Contaktly is enough: the deliveries still waiting go to the new one. Switching Send automatically off, or removing the address, cancels them. Sends from Identify and Send an example are not retried, because you see the answer straight away.

The Integrations card shows the last delivery and what your URL answered, or when we will try again.

Duplicates

The same company can reach you more than once: once per visit, and again if we retried a delivery whose answer was lost on the way back. Keep the X-Contaktly-Delivery ids you have processed and skip a repeat, and match companies on company.domain so a return visit updates the one you have.

Security

  • We only send to https addresses, and refuse addresses with a password in them, bare IP addresses and localhost.
  • Check the signature on every delivery, as above. It proves the body came from us and that nobody changed it.
  • sentAt is signed too. To refuse a replayed delivery, reject any whose sentAt is more than five minutes old. Retries are built fresh, so they pass.
  • Our servers run on Vercel in Frankfurt and have no fixed IP addresses, so an allow-list of addresses will not work. The signature is the check.
  • Keep the secret out of your code and out of version control. Saving a new address issues a new secret and the old one stops working at once. Removing the address deletes the secret.

Versioning

version is 1. Within version 1 we only add fields: we never rename one, remove one or change what it means. Ignore fields you do not recognise and your receiver keeps working. A change that would break a receiver would come as version 2, announced to every workspace with a webhook well ahead, never shipped quietly.

Changes

  • 23/09/2026Automatic sends are retried for 36 hours, with the new X-Contaktly-Attempt header. Automatic sends wait for the people search, so they carry the people. This page.
  • 17/09/2026Webhooks launched.

Stuck, or something here is unclear? Email support@contaktly.com with the X-Contaktly-Delivery id of the delivery and we can trace exactly what happened to it.