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:
| Header | Content |
|---|---|
webhook-id | Unique event id (idempotency in your receiver). |
webhook-timestamp | Unix timestamp of the delivery. |
webhook-signature | v1,<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):
Node.js
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.