Skip to main content

Use incoming webhooks to get real-time updates

Listen for events on your Kavitro account so your integration can automatically trigger reactions.

They work by sending out an HTTP request to a specified endpoint when an event is triggered. This makes webhooks ideal for integrating two applications.

Webhooks are a performant alternative to continuously polling for changes. Webhooks remove the need to send requests over and over. Instead, the service provider sends notifications whenever updated data is available.

How we use webhooks

A webhook enables Kavitro to push real-time notifications to your endpoint as a JSON payload. You can then use these notifications to execute actions in your backend systems.

How to receive webhooks

  1. Build an endpoint that accepts POST requests with a JSON body.
  2. Verify the signature, then acknowledge with a 2xx before doing any real work.
  3. Deploy it at a publicly accessible HTTPS URL.
  4. Register that URL, either in Settings > Integrations or through the API.

The rest of this page walks through each of those.

Step 1: Create a webhook endpoint

Set up an endpoint on your server that accepts unauthenticated POST requests and expects the data as a JSON payload. It has to be reachable over HTTPS from the public internet.

Step 2: Handle requests

Kavitro sends events to your webhook endpoint as part of a POST request with a JSON payload.

Check event objects

Each event is structured as an event object with an id, type, occurredAt timestamp and related resource nested under data. Your endpoint must check the event type and parse the payload of each event.

[
{
"id": "256a5a1f-3c1f-4a87-9e37-0008be001ade",
"type": "task.updated",
"data": {
"object": {
"id": "9",
"type": "task"
}
},
"occurredAt": "2023-07-09T10:52:24.000000Z"
}
]

The payload is always a JSON array, but it currently carries exactly one event per request. Parse it as an array anyway, so your endpoint keeps working if that ever changes.

The payload tells you what changed, not what it changed to

data.object carries only an id and a type. It is a pointer to the record, not a copy of it - there are no field values in a webhook, and no before/after comparison.

So the normal shape of a handler is: read type and data.object.id, then query the API for whatever you actually need about that record. Doing it that way also means you always act on the record's current state rather than on a snapshot that may already be stale by the time you process it.

Important!

There's an exception for files, where data.object.id carries the file's UUID rather than its numeric ID. The top-level event id is always a UUID, for every event type.

The request we send

Deliveries are POST requests with a JSON body and these characteristics:

MethodPOST
Content-Typeapplication/json
X-Kavitro-SignatureHMAC-SHA256 of the body, see verifying requests
Timeout3 seconds
BodyA JSON array holding one event object

There is no timestamp header and no custom user agent, so the signature is the only thing that authenticates a delivery.

Return a 2xx response

Your endpoint must quickly return a successful HTTP status code (2xx) prior to any complex logic that could cause a timeout.

Verify requests

To ensure that the requests you're getting at your webhook endpoint are actually coming from Kavitro, Kavitro sends an X-Kavitro-Signature header containing an HMAC-SHA256 of the request body, keyed with your endpoint's secret.

Learn more about validating signature requests.

Retries

If your server has problems handling notifications at any time, Kavitro retries the delivery. A notification is attempted at most 10 times in total: the original delivery plus 9 retries.

If a 2xx HTTP status code isn't received within 3 seconds, or a status code other than 2xx is returned, we assume the delivery was unsuccessful and retry it using exponential backoff. After the 10th failed attempt the notification is discarded.

Kavitro uses exponential backoff to avoid overwhelming a struggling endpoint. Each wait below is the delay after the preceding attempt fails. So if the original delivery gets no 2xx within 3 seconds, the first retry follows 10 seconds later; if that also fails, the next comes 100 seconds after it, and so on.

RetryWait before it
110 seconds
2100 seconds
31,000 seconds (about 17 minutes)
410,000 seconds (about 2.8 hours)
5100,000 seconds (about 28 hours)
6100,000 seconds (about 28 hours)
7100,000 seconds (about 28 hours)
8100,000 seconds (about 28 hours)
9100,000 seconds (about 28 hours)

An endpoint that never recovers is therefore retried over roughly 5.9 days before the notification is dropped.

Event types

An event's type is the name of the resource and the action, joined with a dot: task.created, contact.updated, file.deleted.

Actions

Every resource emits the same four actions.

ActionSent when
createdThe record is created.
updatedThe record is changed. See the exception below.
deletedThe record is deleted. Deletes are reversible, so this is not the end.
restoredA previously deleted record is restored.
Important!

No updated event is sent when the save touched a rich-text description or terms field. This applies whenever one of those fields is among the changes, not only when it is the only change - so editing a description, on its own or alongside other fields, produces no webhook at all.

Resources

ResourceResourceResource
actletterSourcespecification
actTypeletterTypespecificationType
contactmeterstatus
contractmeterManufacturerstructure
contractTypemeterModelsubmission
coordinationmeterNominalValuesubmissionSource
coordinationStageordinancesubmissionType
coordinationTypeordinanceTypetag
easementpriceListtask
easementTypeproducttaskType
eSigningproductGrouptaxRate
expenseprojecttemplate
filepropertytypeGroup
invoicequote
letterreading

One event does not follow the pattern: dbMirror.completed is sent when a database mirror finishes, and carries no data.object.

note

You cannot subscribe to a subset of these. Every enabled endpoint on an account receives every event the account produces, so check type in your handler and ignore what you don't need.

Managing webhooks through the API

Endpoints do not have to be set up by hand. The same registration you can do under Settings > Integrations is available as ordinary mutations, which is what you want if you provision endpoints per environment or per customer.

mutation CreateWebhook($input: CreateWebhookInput!) {
createWebhook(input: $input) {
id
url
secret
isEnabled
}
}
Query variables
{
"input": {
"name": "Production listener",
"url": "https://example.com/kavitro/webhooks",
"isEnabled": true
}
}

url is the only required field. The secret is generated for you and can be read back at any time, so you never have to copy it out of the interface by hand.

updateWebhook, deleteWebhook and restoreWebhook round out the set, and webhooks lists what an account has registered. Setting isEnabled: false is the way to pause deliveries without discarding the endpoint or its secret.

Delivery logs

Every delivery attempt is recorded, and the log is queryable. This is the first place to look when an endpoint appears to be missing events:

query WebhookLogs($id: ID!) {
webhook(id: $id) {
url
logs(first: 25, orderBy: [{ column: CREATED_AT, order: DESC }]) {
edges {
node {
eventId
type
attempt
statusCode
message
createdAt
}
}
}
}
}

attempt tells you which try you are looking at, so a row with a high attempt is a delivery that has been retried. statusCode and message record what your endpoint answered. Filter on them with where to pull out just the failures:

logs(first: 25, where: { column: STATUS_CODE, operator: GTE, value: 400 }) {
edges {
node {
type
attempt
statusCode
message
}
}
}

Logs are retained for 90 days.

Limitations

These are properties of the system, not things you can configure around. Each one has a corresponding habit in best practices.

  • Delivery is "at least once". An endpoint may receive the same event more than once, so handlers need to be idempotent. Duplicates share the same id.
  • Delivery is not guaranteed. Once the retry schedule is exhausted the notification is dropped, so webhooks alone are not a complete record of what happened.
  • Order is not guaranteed. Events can arrive out of sequence, and retries make that more likely. Use each event's occurredAt rather than arrival order.
  • Not every change raises an event. A save that touches a description or terms field produces no updated event. See event types.
  • Events carry no field values. data.object is a pointer to the record; query the API for its contents.
  • Timing is typical, not contractual. Notifications usually arrive well under 60 seconds after the event, but that is not a guarantee.
  • Logs are kept for 90 days. Delivery logs are permanently deleted after that and cannot be recovered.