KasarKasar Docs
API Reference

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 (http or https). URLs that point to a private or internal network are refused, at creation and again before every delivery;
  • an authentication mode: none, bearer, header or hmac (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:

StatusMeaning
draftCreated, nothing delivered yet
activeIts last delivery (or test) succeeded
pausedDeliveries are held until it is resumed (see Paused destinations)
failing5 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}.deleted

For 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:

Eventdata.objectdata.extra keys
pipeline.step_changedthe object whose pipeline movedpipelineFieldName, fromStepId, toStepId
opportunity.wonsamesame
opportunity.lostsamesame
opportunity.reopenedsamesame
list.member_addedthe object of the record addedlistId
list.member_removedthe object of the record removedlistId
email.receivedcontactsinteractionId, channel
email.sentcontactsmessageSendId, channel
email.repliedcontactsinteractionId, messageSendId
email.bouncedcontactsrecipient
email.unsubscribedcontactsrecipient
linkedin.message_receivedcontactsinteractionId, channel
linkedin.message_sentcontactsmessageSendId, channel
whatsapp.message_receivedcontactsinteractionId, channel
whatsapp.message_sentcontactsmessageSendId, channel
call.completedcontactsmeetingId, direction, outcome, duration
call.missedcontactsmeetingId, direction, outcome
call.recording_readycontactsmeetingId, 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=
HeaderDescription
webhook-idIdentifies 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-timestampWhen this attempt was sent, in Unix seconds. It changes from one attempt to the next
webhook-signatureauth_kind: hmac only. See Verifying signatures
Content-Typeapplication/json
User-AgentKasar-Webhooks/1.0 (+https://kasar.app)
Authorizationauth_kind: bearer only: Bearer <secret>
your headerauth_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"]
  }
}
KeyDescription
typeThe event key
timestampWhen the change happened in Kasar (ISO 8601, UTC). Fixed: a retry sends the same value
data.objectThe object, by its plural name
data.idThe record id. Always set, including on deleted
data.recordThe 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.extraBusiness 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.

ChangeMessages
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 recordsThe 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 onescreated 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_body and response_body are null, and bodies_withheld is true.

"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:

  1. The signed content is {webhook-id}.{webhook-timestamp}.{raw body}, joined by dots.
  2. The key is the secret without its whsec_ prefix, base64-decoded.
  3. HMAC-SHA256 of the signed content with that key, encoded in base64.
  4. 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 stale

Responding

Your responseWhat Kasar does
Any 2xx within 20 secondsDelivered
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 failureRetried on the schedule below
A non-2xx status with the header webhook-delivery: abort-messageStopped: 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:

AttemptSent after the previous failure
1immediately
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 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-id values you processed and answer 2xx without 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, the timestamp of the last message you applied, and ignore a message with an older one. Keep deletions with their timestamp too, so that a late updated does 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.

MethodPathDescription
GET/webhooks/destinationsList destinations
POST/webhooks/destinationsCreate 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}/pausePause
POST/webhooks/destinations/{id}/resumeResume
POST/webhooks/destinations/{id}/rotate-secretRotate the signing secret
POST/webhooks/destinations/{id}/testSend a test call
GET/webhooks/destinations/{id}/deliveriesList the last 50 deliveries
POST/webhooks/destinations/{id}/deliveries/{delivery_id}/retryReplay 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
}
FieldDescription
idDestination id
nameDisplay name
base_urlURL messages are posted to, returned as stored
auth_kindnone, bearer, header or hmac
auth_header_nameWith header: the header that carries the secret. Otherwise null
has_secretWhether a secret (token or signing secret) is set
eventsSubscribed event keys
statusdraft, active, paused or failing
consecutive_failuresDeliveries 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.

FieldRequiredDescription
nameyesDisplay name, non-empty
base_urlyeshttp or https URL. Refused if it is invalid, does not resolve, or resolves to a private or internal address
auth_kindnonone (default), bearer, header or hmac
auth_header_namewith headerName 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
secretwith bearer and headerThe token your system issued. Stored encrypted, never returned. Refused with hmac: Kasar generates the signing secret
eventsnoEvent 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: hmac generates a signing secret, returned once as signing_secret in this response. A destination that already signs keeps its secret.
  • Leaving hmac drops the signing secret. A token survives a switch between bearer and header.
  • bearer and header still 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.

FieldDescription
successThe attempt was delivered
status_codeYour HTTP status, null when no response came back or nothing was sent
error_codenull on success, otherwise one of the codes below
error_messageHuman-readable detail. Do not parse it: use error_code
eventEvent key (kasar.test for a test call)
delivery_idThe 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_bodyThe 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_truncatedtrue when request_body is only the start of the body
request_body_sizeThe size of the body sent, in bytes
request_body_redactedtrue when secrets were masked in request_body: it is then not the bytes sent
response_bodyThe start of your response (4 KB), as a string, values that look like secrets masked
response_body_truncatedtrue when your response was longer
bodies_withheldtrue when the delivery was sent before a field became confidential: request_body and response_body are then null
error_codeMeaningReplayable
delivery_failedThe attempt failed; another one follows
delivery_abandonedThe last attempt failed, or the destination stayed paused more than 30 daysyes
delivery_abortedYou answered webhook-delivery: abort-messageyes
delivery_refusedThe address is refused (invalid, or resolves to a private network)yes
destination_unauthenticatedThe authentication mode requires a secret the destination does not haveyes
destination_goneThe destination was deleted after the message was queued
destination_disabledA test call to a paused destination
integration_revokedNot sent: the attached integration is revoked
object_not_grantedNot sent: the attached integration does not read this object
object_not_servedNot sent: this object is not served
record_goneNot sent: the record was deleted or became invisible before it was read
prepared_before_confidentialNot 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.

StatusCodeWhen
400INVALID_BODYThe body is missing, is not JSON, or is not a JSON object
400DESTINATION_LIMIT_REACHEDThe workspace already has 200 destinations
403FORBIDDEN_GRANTThe integration is not granted webhooks:manage
404NOT_FOUNDUnknown or malformed destination id, the id of an inbound webhook, an unknown action, or a delivery no longer in the queue
409DESTINATION_IN_USEDeleting a destination a workflow calls
409CONFLICTReplaying a delivery that has not ended in failure
413PAYLOAD_TOO_LARGEBody above 256 KB
422VALIDATION_ERRORA 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

On this page