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
- Build an endpoint that accepts
POSTrequests with a JSON body. - Verify the signature, then acknowledge with a 2xx before doing any real work.
- Deploy it at a publicly accessible HTTPS URL.
- 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.
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.
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:
| Method | POST |
Content-Type | application/json |
X-Kavitro-Signature | HMAC-SHA256 of the body, see verifying requests |
| Timeout | 3 seconds |
| Body | A 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.
| Retry | Wait before it |
|---|---|
| 1 | 10 seconds |
| 2 | 100 seconds |
| 3 | 1,000 seconds (about 17 minutes) |
| 4 | 10,000 seconds (about 2.8 hours) |
| 5 | 100,000 seconds (about 28 hours) |
| 6 | 100,000 seconds (about 28 hours) |
| 7 | 100,000 seconds (about 28 hours) |
| 8 | 100,000 seconds (about 28 hours) |
| 9 | 100,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.
| Action | Sent when |
|---|---|
created | The record is created. |
updated | The record is changed. See the exception below. |
deleted | The record is deleted. Deletes are reversible, so this is not the end. |
restored | A previously deleted record is restored. |
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
| Resource | Resource | Resource |
|---|---|---|
act | letterSource | specification |
actType | letterType | specificationType |
contact | meter | status |
contract | meterManufacturer | structure |
contractType | meterModel | submission |
coordination | meterNominalValue | submissionSource |
coordinationStage | ordinance | submissionType |
coordinationType | ordinanceType | tag |
easement | priceList | task |
easementType | product | taskType |
eSigning | productGroup | taxRate |
expense | project | template |
file | property | typeGroup |
invoice | quote | |
letter | reading |
One event does not follow the pattern: dbMirror.completed is sent when a database mirror finishes, and carries no data.object.
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
}
}
{
"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
occurredAtrather than arrival order. - Not every change raises an event. A save that touches a
descriptionortermsfield produces noupdatedevent. See event types. - Events carry no field values.
data.objectis 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.