Skip to content
Integrations

Webhooks

Last updated: Jul 14, 2026

Webhooks let Uku push changes to your system the moment they happen, so an integration can react to a new client or a changed invoice instead of polling for one.

Company Owners and Company Admins on the Elite plan, using an API v3 key with the All scope (the API calls this the admin scope). See Public API v3 for how to generate the key.

Subscriptions are created and deleted through the API, not in Uku — that’s the job of your integration (or of Zapier or Make on your behalf). Uku shows you what exists and how it’s doing; see Monitoring your webhooks below.

Every endpoint below can also be called from the interactive sandbox at app.getuku.com/api/v3/docs, which is the quickest way to create a test subscription and inspect what comes back.

One subscription is one event going to one URL. To listen for three events, create three subscriptions. Uku does not deduplicate them, so two subscriptions with the same event and URL deliver every event twice — check GET /webhooks before registering the same hook again.

Terminal window
curl -X POST https://app.getuku.com/api/v3/webhooks \
-H "X-Uku-Company: your-company-uuid" \
-H "X-API-Key: uku_live_..." \
-H "Content-Type: application/json" \
-d '{"webhook": "client.created", "url": "https://example.com/hooks/uku"}'

The response contains a signing secret, shown only this once — store it as carefully as the API key itself. If you lose it, call POST /webhooks/{id}/rotate-secret for a new one; there’s no overlap period, so update your receiver at the same time.

These are the events you can subscribe to (GET /webhooks/events returns the same list):

ResourceEvents
Clientsclient.created, client.updated, client.deleted
Contactscontact.created, contact.updated, contact.deleted
Taskstask.created, task.updated, task.deleted
Projectsproject.created, project.updated, project.deleted
Invoicesinvoice.created, invoice.updated
Time entriestime_entry.created

Your URL must be a public address — Uku refuses to deliver to private, internal, or loopback addresses, and never follows redirects.

Each delivery is a POST to your URL. The body carries the event and the record itself, in the same shape the matching GET endpoint returns. For a *.deleted event, data holds only the id.

{
"id": "8f1c2a4e-3b7d-4c21-9f0e-2a6b5d8c1e73",
"event": "client.created",
"occurred_at": "2026-07-14T10:15:33+00:00",
"data": { "id": 123, "name": "Apple Ltd" }
}

Alongside it come these headers:

X-Uku-Event: client.created
X-Uku-Delivery-Id: 8f1c2a4e-3b7d-4c21-9f0e-2a6b5d8c1e73
X-Uku-Webhook-Id: 42
X-Uku-Signature: sha256=<hex>
X-Uku-Timestamp: <unix seconds>
X-Uku-Retry-Num: <n> (only on retries)

Deduplicate on the body’s id. It stays the same across every retry and replay of that event, so a receiver that has already handled an id can safely ignore it a second time.

Every delivery is signed with that subscription’s own secret, so your receiver can prove the call really came from Uku and isn’t a replay. Sign the timestamp and the raw body, then compare:

import hmac, hashlib, time
def verify(secret, raw_body, signature, timestamp, tolerance=300):
if abs(int(time.time()) - int(timestamp)) > tolerance:
return False # too old — reject replays
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)

Compute it over the raw bytes you received, before parsing the JSON — re-serializing changes them. Deliveries older than 5 minutes should be rejected.

When a delivery fails, Uku retries it on a fixed ladder and eventually gives up on a receiver that stays broken. Answer with any 2xx status within 10 seconds. Acknowledge first and do your processing afterwards — a slow receiver reads as a failed one.

  • Retries. A failed delivery is retried after 1 minute, 5 minutes, and 30 minutes — four attempts over roughly 36 minutes, then it stops.
  • No retry if your endpoint returns 400, 401, 403, 404, 405, 410, or 422. Those mean the request will never succeed, so Uku gives up immediately. Return 410 Gone to say “this receiver is retired, stop sending.”
  • Auto-disable. After 10 consecutive failures the subscription is switched off (is_active goes false, and failure_count, last_failure_at, and disabled_at tell you why). Fix your endpoint, then re-enable with PATCH /webhooks/{id} and {"is_active": true} — that also clears the failure count.
  • History and replay. GET /webhooks/{id}/deliveries lists attempts with their response status — one row per attempt, so a retried event appears several times. To send one again, take its event_id and call POST /webhooks/{id}/deliveries/{event_id}/replay. A delivery still working through its retries returns 409 DELIVERY_IN_PROGRESS — let it finish first.
  • Pacing is not failure. During a bulk operation — importing a thousand clients, say — Uku spaces deliveries out rather than firing them all at once, so some arrive a few minutes late. Pacing starts well above normal traffic (600 deliveries a minute, 50,000 a day per company), it doesn’t consume retries, and it never counts toward the auto-disable limit. Don’t mistake it for your endpoint being down.

Uku shows every webhook subscription your integrations have created, so you can see at a glance whether deliveries are landing — without reading your own server logs.

Path: Settings & AppsPublic APIPublic APIConnected webhooks

The list gives each subscription’s Event, URL, Status, Failures, and Created date. The status tells you where it stands:

  • Active — deliveries are landing.
  • Failing (N) — the last N deliveries in a row failed. Hover the failure count to see when the last one was.
  • Disabled — ten failures in a row, so Uku switched it off and is no longer sending. Nothing is queued while it’s off.

Expand a row to see its recent deliveries, each with the event, the HTTP status your endpoint returned, which attempt it was, and when. A delivery that failed gets a Replay button; a disabled subscription gets a Re-enable button, which also clears its failure count.

My webhook subscription stopped receiving anything

Section titled “My webhook subscription stopped receiving anything”

A webhook subscription that goes quiet has almost always been disabled by Uku after repeated delivery failures. Check it in Settings & AppsPublic APIPublic APIConnected webhooks. Disabled means ten deliveries in a row failed and Uku switched it off. Fix the receiving endpoint, then click Re-enable — that clears the failure count and delivery resumes. (From code, the same thing is GET /webhooks/{id} and PATCH /webhooks/{id} with {"is_active": true}.)

Deliveries that were already retrying when it stopped are kept — you’ll find them in the list and can Replay them. But events that happened after it was disabled were never recorded at all, so nothing will arrive for that window: read those records from the API directly to catch up.

Uku only delivers to public internet addresses. A localhost URL, an internal hostname, a private range such as 10.x or 192.168.x, or anything that resolves to one of those is rejected when you create the subscription. For local development, put a tunnelling service in front of your machine so the URL is publicly reachable.

Receiving the same event twice is expected and safe to handle: a delivery that times out may still have reached you, so Uku retries it. Every copy of an event carries the same id in the body — treat that as the key, and ignore an id you’ve already processed.