Skip to Content
Webhooks

Webhooks

Instead of polling, subscribe to outbound events: Kinmu sends a POST to your HTTPS endpoint when something relevant happens. Every delivery is signed with the Standard Webhooks  convention — always verify the signature before processing the event.

Endpoint management requires the webhooks:manage scope and is done from the dashboard or via the /v1/webhook-endpoints endpoints.

Subscription

Create an endpoint specifying your URL (HTTPS) and the events you care about:

curl -s -X POST "https://api.kinmu.app/v1/webhook-endpoints" \ -H "Authorization: Bearer kinmu_sk_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://tu-app.example.com/webhooks/kinmu", "events": ["absence.approved", "checkin.created"], "description": "Sync with our ERP" }'

The response includes the signing secret (whsec_…), visible only once. Store it: you need it to verify signatures.

Management available: GET (list), GET /{id}, PATCH /{id} (events/status), DELETE /{id}, POST /{id}/test (sends a ping), GET /{id}/deliveries (recent deliveries) and POST /{id}/deliveries/{deliveryId}/retry (re-queues a specific delivery). Maximum 5 active endpoints per company.

Events

employee.created, employee.updated, employee.terminated, checkin.created, absence.requested, absence.approved, absence.denied, absence.cancelled, vacation_balance.updated, report.completed, ping.

Event payload

{ "id": "evt_…", "type": "absence.approved", "created_at": "…", "company_id": "…", "data": { } }

data carries public ids + status (minimal); rehydrate the full resource via GET when you need all the fields.

Signature verification

Every delivery arrives with three headers:

HeaderContent
webhook-idUnique event id (idempotency in your receiver).
webhook-timestampUnix timestamp of the delivery.
webhook-signaturev1,<signature> (there may be several, separated by spaces).

Algorithm: signature = base64( HMAC_SHA256( key, "{webhook-id}.{webhook-timestamp}.{raw_body}" ) ), prefixed with v1,. The key is the part after whsec_ of the secret (shown only once when the webhook is created), decoded from base64 to bytes. Sign over the raw body (exact bytes received), not over the re-serialized JSON.

Verification (reject if it does not match or if webhook-timestamp differs by > 5 min from your clock):

const crypto = require('crypto'); function verify(secret, headers, rawBody) { const id = headers['webhook-id']; const ts = headers['webhook-timestamp']; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // ±5 min const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = 'v1,' + crypto .createHmac('sha256', key) .update(`${id}.${ts}.${rawBody}`) .digest('base64'); return headers['webhook-signature'] .split(' ') .some(sig => crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))); }

Verify and sign over the raw body (exact bytes received), not over the re-serialized JSON — any reformatting invalidates the signature. Use the webhook-id as an idempotency key in your receiver to discard resends.

Retries

On failure (or a response ≠ 2xx) Kinmu retries with backoff: 1 m, 5 m, 30 m, 2 h, 12 h, 24 h. Respond 2xx quickly; process async if you’re slow. An endpoint that fails for 7 days in a row is auto-disabled.

You can manually re-queue a failed delivery with POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/retry (use the delivery id you see in GET /{id}/deliveries).

Best practices

Respond fast, process in the background

Verify the signature, enqueue the work and respond 2xx immediately.

Always verify the signature

Don’t process any payload without verifying webhook-signature. Treat the secret as a credential.

Be idempotent

Deduplicate by webhook-id. Retries are normal.

Test with ping

Use POST /v1/webhook-endpoints/{id}/test to receive a ping event and validate your integration end to end.

Last updated on