Webhooks
Outbound webhooks. Subscribe a destination to record and business events, verify Standard Webhooks signatures, handle retries, and manage destinations through the API.
Outbound webhooks tell your systems when data changes in Kasar. You declare a destination (a URL, an authentication mode, a list of events), and Kasar sends one POST per matching event. Messages follow Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature headers, and a { type, timestamp, data } body.
Destinations are managed in the app (Settings, Webhooks) or with the management API at the end of this page.
Concepts
Destination
A destination is:
- a URL (
httporhttps). URLs that point to a private or internal network are refused, at creation and again before every delivery; - an authentication mode:
none,bearer,headerorhmac(signed, see Verifying signatures); - a list of events it subscribes to;
- optionally, an API integration it reads under.
A destination attached to an integration receives exactly what that integration's key can read: the same objects and the same fields. Its record is identical to what GET /objects/{object}/records/{id} returns with that key. The integration's permissions are re-read for every event: restricting or revoking the integration applies from the next event. A destination created through the API is always attached to the integration whose key created it. A destination created in Settings can be attached to an integration, or left unattached, in which case it receives every object the API serves, with every field except confidential ones.
Its status is one of:
| Status | Meaning |
|---|---|
draft | Created, nothing delivered yet |
active | Its last delivery (or test) succeeded |
paused | Deliveries are held until it is resumed (see Paused destinations) |
failing | 5 deliveries in a row ended in failure. It keeps receiving |
Events
Record events exist for every object the API serves (the objects listed by GET /objects, custom objects included):
{object}.created
{object}.updated
{object}.deletedFor example opportunities.updated, companies.deleted, sites.created.
Business events describe something that is not a plain field change. They are offered for subscription as long as their object is served:
| Event | data.object | data.extra keys |
|---|---|---|
pipeline.step_changed | the object whose pipeline moved | pipelineFieldName, fromStepId, toStepId |
opportunity.won | same | same |
opportunity.lost | same | same |
opportunity.reopened | same | same |
list.member_added | the object of the record added | listId |
list.member_removed | the object of the record removed | listId |
email.received | contacts | interactionId, channel |
email.sent | contacts | messageSendId, channel |
email.replied | contacts | interactionId, messageSendId |
email.bounced | contacts | recipient |
email.unsubscribed | contacts | recipient |
linkedin.message_received | contacts | interactionId, channel |
linkedin.message_sent | contacts | messageSendId, channel |
whatsapp.message_received | contacts | interactionId, channel |
whatsapp.message_sent | contacts | messageSendId, channel |
call.completed | contacts | meetingId, direction, outcome, duration |
call.missed | contacts | meetingId, direction, outcome |
call.recording_ready | contacts | meetingId, direction |
A key is present in extra only when the event carries it. The content of a message (its subject, its body) is never sent: only the fact that it exists.
Subscribing to a key that is not offered is refused with 422 VALIDATION_ERROR, which names the unknown keys.
What Kasar sends
Request
POST /hooks/kasar HTTP/1.1
Content-Type: application/json
User-Agent: Kasar-Webhooks/1.0 (+https://kasar.app)
webhook-id: 9f2c0d3e5b1a...
webhook-timestamp: 1760361532
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=| Header | Description |
|---|---|
webhook-id | Identifies the message. The same on every retry and on a manual replay: deduplicate on it. Each destination gets its own id for the same event |
webhook-timestamp | When this attempt was sent, in Unix seconds. It changes from one attempt to the next |
webhook-signature | auth_kind: hmac only. See Verifying signatures |
Content-Type | application/json |
User-Agent | Kasar-Webhooks/1.0 (+https://kasar.app) |
Authorization | auth_kind: bearer only: Bearer <secret> |
| your header | auth_kind: header only: the header you named, carrying the secret |
webhook-id and webhook-timestamp are sent whatever the authentication mode. The query string of the destination URL is kept as is.
Body
{
"type": "opportunities.updated",
"timestamp": "2026-10-09T14:28:52.676Z",
"data": {
"object": "opportunities",
"id": "221218dc-74eb-4cfd-a00b-6703b356f324",
"record": {
"id": "221218dc-74eb-4cfd-a00b-6703b356f324",
"name": "Lagrange, multi-site contract 2027",
"status": "won",
"client_company_id": "9de44c07-c161-46db-9a7d-73e140428271",
"sites": [
{ "id": "8e340752-6c51-4754-816a-014792ac7b1a" },
{ "id": "23c314da-1760-4974-965b-55215fb5d990" }
],
"created_at": "2026-10-09T13:42:38.045Z",
"updated_at": "2026-10-09T14:28:52.676Z"
},
"changed_fields": ["sites"]
}
}| Key | Description |
|---|---|
type | The event key |
timestamp | When the change happened in Kasar (ISO 8601, UTC). Fixed: a retry sends the same value |
data.object | The object, by its plural name |
data.id | The record id. Always set, including on deleted |
data.record | The record, see below. null on {object}.deleted |
data.changed_fields | {object}.updated only: the record keys the change touched |
data.merged_into | {object}.deleted only, when the record was merged into another one: the id of the record kept |
data.extra | Business events only: the context listed in the events table |
A deletion:
{
"type": "contacts.deleted",
"timestamp": "2026-10-09T15:02:11.204Z",
"data": { "object": "contacts", "id": "de38a818-6ff0-48fd-9eb2-2b754cff141d", "record": null }
}A merge reads the same, with "merged_into": "<id of the record kept>" in data.
A business event:
{
"type": "opportunity.won",
"timestamp": "2026-10-09T13:42:38.979Z",
"data": {
"object": "opportunities",
"id": "221218dc-74eb-4cfd-a00b-6703b356f324",
"record": { "id": "221218dc-74eb-4cfd-a00b-6703b356f324", "status": "won" },
"extra": { "pipelineFieldName": "pipeline", "fromStepId": "...", "toStepId": "..." }
}
}The body is built once, when the event is queued: every retry and every manual replay sends the same bytes. The one exception is a field that becomes confidential in between: see below.
The record
data.record is the record in the same machine shape as the API (Records): a relation is its id (client_company_id), a many-to-many link is [{ "id", ...link columns }] (with object on a link that can target several objects), emails and phones are [{ "id", "email_address" | "phone_number", "label", "is_primary" }], numbers are JSON numbers, and an empty string is never sent (null instead). The inverse side of a one-to-many relation (a company's opportunities, for instance) is not part of the record.
It is limited to what the destination may read: for a destination attached to an integration, the objects and fields that integration is granted. A confidential field is never in it, nor a field computed from one.
Linked records are never embedded: an opportunity carries the ids of its company and sites, and each of those arrives in its own message. If you receive an id you do not know yet, read it with GET /objects/{object}/records/{id}.
The record is read when Kasar prepares the message, right after the change. It always includes the change it announces, and can include a later one made in between.
changed_fields uses the record's keys (name, client_company_id, sites, status...). It only lists keys this destination can read, and it is never empty: when a change touches only fields the destination does not receive (confidential ones included), no message is sent to it.
Which changes emit what
One message per record and per change. A change is announced whatever made it: the app, the API (your own key included, so expect the events of what you write back), an import, a sync, an automation, an inbound webhook.
| Change | Messages |
|---|---|
| Create a record | {object}.created. Its links, emails and phones are already in that record: no updated follows for it |
| Edit fields | {object}.updated, only if a value actually changes. Writing back the same value sends nothing |
| Add or remove a many-to-many link (a site on an opportunity, a contact in a company) | updated for each record whose object has that link as a field, so usually both sides |
Edit a link's own column (a contact's position in a company) | updated for both sides, when a value actually changes |
Set or clear a relation (an opportunity's client_company_id) | updated for the record that holds the relation only |
| Add or remove an email, phone or LinkedIn profile | {object}.updated for the record |
| Delete a record | {object}.deleted for it, and no updated just before. Records linked to it get updated (the link is gone), and records that pointed to it through an optional relation get updated with that relation set to null |
| Merge two records | The absorbed record gets {object}.deleted with data.merged_into. Links moved by the merge are announced as link changes |
| Create a record linked to existing ones | created for the new record, updated for the existing records on the other side of a many-to-many link |
A record that is created and deleted before its created is read sends only its deleted: you can receive a deleted for an id you never saw. Ignore it.
To catch up on deletions you missed, GET /objects/{object}/deleted?since=<ISO 8601> lists the records deleted in the last 30 days.
When nothing is sent
An event a destination subscribes to is not sent, and the destination's delivery log says why, when:
- the attached integration is revoked (
integration_revoked); - the attached integration does not read the object (
object_not_granted); - the object is no longer served (
object_not_served); - the record was deleted, or became invisible, before it could be read (
record_gone).
An {object}.updated is also not sent, without a log line, when the change touched none of the fields the destination reads: adding a site to an opportunity sends sites.updated only to a destination that reads the site's list of opportunities.
Confidential fields and what was already sent
When an admin marks a field confidential, messages already prepared may carry it. From then on:
- a delivery queued before the marking (including one being retried, held by a pause, or replayed) is not sent. Its log line says
prepared_before_confidential, without a body, and it cannot be replayed: the next change of the record sends a new message; - the log no longer serves the bodies and responses of deliveries sent before the marking:
request_bodyandresponse_bodyarenull, andbodies_withheldistrue.
"Before" counts a 30-second margin after the marking, the time every Kasar server takes to apply it.
Verifying signatures
A destination with auth_kind: hmac signs every message. Kasar generates the secret (whsec_ followed by base64), shows it once at creation or rotation, and never returns it again.
The signature is computed as follows:
- The signed content is
{webhook-id}.{webhook-timestamp}.{raw body}, joined by dots. - The key is the secret without its
whsec_prefix, base64-decoded. - HMAC-SHA256 of the signed content with that key, encoded in base64.
- The header carries it prefixed with its version:
v1,<signature>.
webhook-signature can carry several signatures separated by a space. After a secret rotation, the previous secret keeps signing for 24 hours next to the new one:
webhook-signature: v1,<signed with the new secret> v1,<signed with the previous secret>Accept the message if any v1 signature matches.
Verify against the raw bytes of the body, before any JSON parsing: re-serializing changes spacing or key order and breaks the signature. Reject a webhook-timestamp more than 5 minutes away from your clock (the Standard Webhooks default): it is the time of the attempt, so a retry sent hours later still passes.
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_SECONDS = 5 * 60
export function verifyKasarWebhook(rawBody, headers, secret) {
const id = headers['webhook-id']
const timestamp = headers['webhook-timestamp']
const signatures = headers['webhook-signature']
if (!id || !timestamp || !signatures) return false
const now = Math.floor(Date.now() / 1000)
if (Math.abs(now - Number(timestamp)) > TOLERANCE_SECONDS) return false
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest()
return signatures.split(' ').some((entry) => {
const [version, signature] = entry.split(',')
if (version !== 'v1' || !signature) return false
const received = Buffer.from(signature, 'base64')
return received.length === expected.length && timingSafeEqual(received, expected)
})
}With Express, keep the body raw on that route:
app.post('/hooks/kasar', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body.toString('utf8')
if (!verifyKasarWebhook(rawBody, req.headers, process.env.KASAR_WEBHOOK_SECRET)) {
return res.status(401).end()
}
const event = JSON.parse(rawBody)
// ... process event
res.status(200).end()
})Any Standard Webhooks implementation works as is. With the official standardwebhooks library, pass the secret exactly as Kasar gave it:
import { Webhook } from 'standardwebhooks'
const wh = new Webhook(process.env.KASAR_WEBHOOK_SECRET) // "whsec_..."
const event = wh.verify(rawBody, req.headers) // throws if no signature matches or the timestamp is staleResponding
| Your response | What Kasar does |
|---|---|
Any 2xx within 20 seconds | Delivered |
Any other status (3xx, 4xx including 401 and 429, 5xx) | Retried on the schedule below |
| No response within 20 seconds, connection or TLS error, DNS failure | Retried on the schedule below |
A non-2xx status with the header webhook-delivery: abort-message | Stopped: no further attempt |
Redirects are not followed: a 3xx is a failure. Because 4xx responses are retried too, a configuration mistake on your side (a signature you reject, an expired token) loses nothing within the schedule: fix it and the next attempt goes through.
Apart from the status and the webhook-delivery header, Kasar only keeps the first 4 KB of your response body, in the destination's delivery log.
Retry schedule
8 attempts in total, over about 27 and a half hours:
| Attempt | Sent after the previous failure |
|---|---|
| 1 | immediately |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 | 10 hours |
| 8 | 10 hours |
These delays are minimums: an attempt can wait a little longer when the destination already has deliveries in flight. Every attempt carries the same webhook-id and body, with a fresh webhook-timestamp and signature.
A delivery whose 8th attempt fails is abandoned, and so is one you aborted with webhook-delivery: abort-message. Neither is lost: it stays replayable for 30 days, from Settings or with POST .../deliveries/{delivery_id}/retry, with the same webhook-id and body.
Duplicates and ordering
- Duplicates. A message can arrive more than once (a retry after a response that never reached Kasar, a manual replay). Keep the
webhook-idvalues you processed and answer2xxwithout reprocessing a known one. Each message carries the full record, so applying it twice gives the same result anyway. - Order is not guaranteed. Kasar sends up to 4 deliveries at the same time to one destination (and up to 6 across all destinations of a workspace), and a retried message can be overtaken by a later one. Messages about different records, such as an opportunity and the site it references, can arrive in any order.
- Use
timestamp, per record. Keep, for each record id, thetimestampof the last message you applied, and ignore a message with an older one. Keep deletions with theirtimestamptoo, so that a lateupdateddoes not bring a deleted record back.
Paused and failing destinations
Paused. Nothing is sent, nothing counts as a failure, and events keep being queued: they are held for up to 30 days. On resume, held deliveries are released at once, roughly in the order they were emitted, each one describing the record as it was at its change. A delivery held more than 30 days is abandoned, and stays replayable.
Failing. A destination becomes failing after 5 deliveries in a row end in failure (abandoned after their 8th attempt, or stopped). A delivery that fails a few times and then succeeds does not count. A failing destination keeps receiving; members whose role can manage webhooks are notified, at most once a day per destination. The next successful delivery sets it back to active.
Managing destinations with the API
Base URL: https://platform.kasar.app/api/v1.
Every route below requires the integration to be granted webhooks:manage (Settings, API; see Authentication). Without it: 403 FORBIDDEN_GRANT.
These routes only see the destinations attached to your integration: the ones its keys created, and the ones created in Settings and attached to it. An unattached destination, or one attached to another integration, does not exist for your key: its id answers 404 NOT_FOUND, like an unknown id, and the list does not show it. Secrets are write-only: a destination says has_secret, never the secret.
| Method | Path | Description |
|---|---|---|
GET | /webhooks/destinations | List destinations |
POST | /webhooks/destinations | Create a destination |
GET | /webhooks/destinations/{id} | Get a destination |
PATCH | /webhooks/destinations/{id} | Update a destination |
DELETE | /webhooks/destinations/{id} | Delete a destination |
POST | /webhooks/destinations/{id}/pause | Pause |
POST | /webhooks/destinations/{id}/resume | Resume |
POST | /webhooks/destinations/{id}/rotate-secret | Rotate the signing secret |
POST | /webhooks/destinations/{id}/test | Send a test call |
GET | /webhooks/destinations/{id}/deliveries | List the last 50 deliveries |
POST | /webhooks/destinations/{id}/deliveries/{delivery_id}/retry | Replay a delivery that ended in failure |
The destination object
{
"id": "3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90",
"name": "ERP sync",
"base_url": "https://erp.example.com/hooks/kasar",
"auth_kind": "hmac",
"auth_header_name": null,
"has_secret": true,
"events": ["opportunities.created", "opportunities.updated", "opportunities.deleted"],
"status": "active",
"consecutive_failures": 0
}| Field | Description |
|---|---|
id | Destination id |
name | Display name |
base_url | URL messages are posted to, returned as stored |
auth_kind | none, bearer, header or hmac |
auth_header_name | With header: the header that carries the secret. Otherwise null |
has_secret | Whether a secret (token or signing secret) is set |
events | Subscribed event keys |
status | draft, active, paused or failing |
consecutive_failures | Deliveries in a row that ended in failure |
List destinations
curl https://platform.kasar.app/api/v1/webhooks/destinations \
-H "Authorization: Bearer ksr_..."Returns { "data": [ <destination>, ... ] }, newest first.
Create a destination
curl -X POST https://platform.kasar.app/api/v1/webhooks/destinations \
-H "Authorization: Bearer ksr_..." \
-H "Content-Type: application/json" \
-d '{
"name": "ERP sync",
"base_url": "https://erp.example.com/hooks/kasar",
"auth_kind": "hmac",
"events": ["opportunities.created", "opportunities.updated", "opportunities.deleted"]
}'{
"data": {
"id": "3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90",
"signing_secret": "whsec_..."
}
}Returns 201. signing_secret is present only with auth_kind: hmac, and only in this response: store it now.
| Field | Required | Description |
|---|---|---|
name | yes | Display name, non-empty |
base_url | yes | http or https URL. Refused if it is invalid, does not resolve, or resolves to a private or internal address |
auth_kind | no | none (default), bearer, header or hmac |
auth_header_name | with header | Name of the header that carries the secret. Refused if it is one Kasar sets itself: webhook-id, webhook-timestamp, webhook-signature, Content-Type, Content-Length, Host, User-Agent, Transfer-Encoding, Connection |
secret | with bearer and header | The token your system issued. Stored encrypted, never returned. Refused with hmac: Kasar generates the signing secret |
events | no | Event keys (default []). Unknown keys are refused |
The destination starts as draft and is attached to the integration whose key created it. A workspace can have at most 200 destinations, paused ones included.
Get a destination
curl https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90 \
-H "Authorization: Bearer ksr_..."Returns { "data": <destination> }. An unknown id, or the id of an inbound webhook, is a 404.
Update a destination
curl -X PATCH https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90 \
-H "Authorization: Bearer ksr_..." \
-H "Content-Type: application/json" \
-d '{ "events": ["opportunities.updated", "companies.updated"] }'Same fields as creation, all optional: a field you leave out does not change, and events replaces the whole list. Returns { "data": <destination> }.
- Switching to
auth_kind: hmacgenerates a signing secret, returned once assigning_secretin this response. A destination that already signs keeps its secret. - Leaving
hmacdrops the signing secret. A token survives a switch betweenbearerandheader. bearerandheaderstill require a secret, given now or already stored.
Delete a destination
curl -X DELETE https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90 \
-H "Authorization: Bearer ksr_..."Returns { "data": { "id": "...", "deleted": true } }. A destination that a workflow calls (its published version or its current draft, paused workflows included) is refused with 409 DESTINATION_IN_USE, which names the workflows: point them elsewhere first.
Pause and resume
curl -X POST https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90/pause \
-H "Authorization: Bearer ksr_..."Returns { "data": { "id": "...", "status": "paused" } }. /resume returns "status": "active", resets consecutive_failures to 0, and releases the held deliveries (see Paused destinations).
Rotate the signing secret
curl -X POST https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90/rotate-secret \
-H "Authorization: Bearer ksr_..."{ "data": { "id": "3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90", "signing_secret": "whsec_..." } }The new secret is returned once. For the next 24 hours, messages carry two signatures, one with the new secret and one with the secret it replaced, so you can deploy the new one without rejecting anything. Rotating again within those 24 hours ends the overlap of the earlier secret. A destination that does not sign (auth_kind other than hmac) is refused with 422.
Send a test call
curl -X POST https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90/test \
-H "Authorization: Bearer ksr_..."{
"data": {
"ok": true,
"status": 200,
"error": null,
"duration_ms": 184,
"sample": { "object": "opportunities", "id": "221218dc-74eb-4cfd-a00b-6703b356f324" }
}
}Sends one request, signed and authenticated like a real delivery, with "type": "kasar.test". Its record is the most recently updated record of the first subscribed object the destination can read, in the shape of a real delivery. When there is none, sample is null and the body is { "type": "kasar.test", "timestamp": "...", "data": {} }.
A test is sent once, without retries, and never counts toward failing. A success sets a draft or failing destination to active. On a paused destination, or one whose integration is revoked, nothing is sent and ok is false. status is the HTTP status you answered, null when no response came back.
List deliveries
curl https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90/deliveries \
-H "Authorization: Bearer ksr_..."{
"data": [
{
"id": "b7e1...",
"created_at": "2026-10-13T09:12:04.118Z",
"success": false,
"status_code": 500,
"duration_ms": 412,
"error_code": "delivery_failed",
"error_message": "...",
"event": "opportunities.updated",
"delivery_id": "9f2c0d3e5b1a...",
"attempt": { "number": 1, "of": 8 },
"request_body": "{\"type\":\"opportunities.updated\",\"timestamp\":\"...\",\"data\":{...}}",
"request_body_truncated": false,
"request_body_size": 812,
"request_body_redacted": false,
"response_body": "Internal Server Error",
"response_body_truncated": false,
"bodies_withheld": false
}
]
}The last 50 log lines of the destination, newest first: one per attempt, per test call, and per event that was not sent.
| Field | Description |
|---|---|
success | The attempt was delivered |
status_code | Your HTTP status, null when no response came back or nothing was sent |
error_code | null on success, otherwise one of the codes below |
error_message | Human-readable detail. Do not parse it: use error_code |
event | Event key (kasar.test for a test call) |
delivery_id | The webhook-id your system received. It is the key for replaying |
attempt | { number, of } for a queued delivery, null for a test call or an event not sent |
request_body | The body sent, as a string. For an event, the exact bytes that were signed, so you can check a signature against it. For a call made by a workflow's HTTP request, values that look like secrets are masked. Above 4 KB, its first 4 KB |
request_body_truncated | true when request_body is only the start of the body |
request_body_size | The size of the body sent, in bytes |
request_body_redacted | true when secrets were masked in request_body: it is then not the bytes sent |
response_body | The start of your response (4 KB), as a string, values that look like secrets masked |
response_body_truncated | true when your response was longer |
bodies_withheld | true when the delivery was sent before a field became confidential: request_body and response_body are then null |
error_code | Meaning | Replayable |
|---|---|---|
delivery_failed | The attempt failed; another one follows | |
delivery_abandoned | The last attempt failed, or the destination stayed paused more than 30 days | yes |
delivery_aborted | You answered webhook-delivery: abort-message | yes |
delivery_refused | The address is refused (invalid, or resolves to a private network) | yes |
destination_unauthenticated | The authentication mode requires a secret the destination does not have | yes |
destination_gone | The destination was deleted after the message was queued | |
destination_disabled | A test call to a paused destination | |
integration_revoked | Not sent: the attached integration is revoked | |
object_not_granted | Not sent: the attached integration does not read this object | |
object_not_served | Not sent: this object is not served | |
record_gone | Not sent: the record was deleted or became invisible before it was read | |
prepared_before_confidential | Not sent: queued before a field became confidential (see above) |
Retry a delivery
curl -X POST https://platform.kasar.app/api/v1/webhooks/destinations/3d2a7c1e-0b5f-4a8e-9c61-2f4e8d7b1a90/deliveries/9f2c0d3e5b1a.../retry \
-H "Authorization: Bearer ksr_..."{ "data": { "delivery_id": "9f2c0d3e5b1a...", "requeued": true } }Replays a delivery that ended in failure (a replayable code above): the same message, same body, same webhook-id, with a full new schedule of 8 attempts. A delivery that is still being retried, or was already replayed, is refused with 409 CONFLICT; calling twice queues it once. A delivery older than 30 days, or that never reached the queue (test calls, events not sent), is a 404.
Errors
Errors use the standard envelope { "error": { "code", "message", "suggestion"? } } (Errors). Calls count toward the rate limits.
| Status | Code | When |
|---|---|---|
400 | INVALID_BODY | The body is missing, is not JSON, or is not a JSON object |
400 | DESTINATION_LIMIT_REACHED | The workspace already has 200 destinations |
403 | FORBIDDEN_GRANT | The integration is not granted webhooks:manage |
404 | NOT_FOUND | Unknown or malformed destination id, the id of an inbound webhook, an unknown action, or a delivery no longer in the queue |
409 | DESTINATION_IN_USE | Deleting a destination a workflow calls |
409 | CONFLICT | Replaying a delivery that has not ended in failure |
413 | PAYLOAD_TOO_LARGE | Body above 256 KB |
422 | VALIDATION_ERROR | A field is invalid: missing name, refused URL, unknown event key, missing or reserved header name, missing token, a secret sent with hmac, rotating a destination that does not sign |