By the end of this article you will know which engine to build on, what a delivery from it looks like, and how to handle one safely.
Who can do this
Section titled “Who can do this”Managing webhooks is open to Company Owners and Company Admins. Company Members cannot.
Path: Settings & Apps → Public API
You also need a key for the engine you are using: an API v3 key with the All scope for v3 webhooks (see Public API v3), or a v1.0 key pair for v1.0 webhooks (see Public API v1.0). The two are not interchangeable.
You need Elite for either engine, but they enforce it differently. The Public API app is an Elite app, and that is where both kinds of key are minted, so without Elite you cannot get a credential at all. API v3 also checks the plan on every request and answers 403 PLAN_UPGRADE_REQUIRED if the company is no longer on Elite. The v1.0 endpoints make no such check, so a key pair issued while a company was on Elite keeps working afterwards.
Subscriptions are always created through the API, never in Uku’s interface. Your integration does it, or Zapier or Make on your behalf.
Which webhook engine to use
Section titled “Which webhook engine to use”Build on API v3. The v3 engine reaches far more of Uku, signs each subscription with its own secret, retries a failed delivery, and keeps a history you can inspect and replay. Use v1.0 only to keep an integration you already have running.
Uku runs the two engines side by side. They share one subscription store but speak different wire formats, and each only ever delivers to its own subscriptions, so a v1.0 subscriber never receives a v3-shaped body.
| API v3 webhooks | API v1.0 webhooks | |
|---|---|---|
| Status | Build here | Live, frozen |
| Subscribe with | POST /api/v3/webhooks | POST /api/v1.0/webhooks/subscription |
| Events | Clients, contacts, tasks, projects, invoices, time entries | Client created, contact created |
| Fires for records created | Through API v3 only | In the Uku web app only |
| Body | { id, event, occurred_at, data } | The record itself, flat |
| Event name header | X-Uku-Event | None |
| Signature header | X-Uku-Signature: sha256=… | X-Webhook-Signature |
| Signing key | A secret minted per subscription | One Uku-wide secret |
| Delivery history | Recorded, viewable, replayable | Not recorded |
The Fires for records created row is the one that surprises people. The two engines listen in almost opposite places. Moving between them changes which records you hear about, not only the shape of the message — see Moving from API v1.0 to API v3.
Events you can subscribe to in API v3
Section titled “Events you can subscribe to in API v3”The v3 catalogue reaches further into Uku than v1.0’s two triggers: clients, contacts, tasks, projects, invoices and time entries, each with created, updated and deleted events.
Three things the names alone will not tell you. Task updates cover status, assignee and date changes, so one subscription is enough to follow a task’s life. There is no invoice-deleted event, because the API exposes no way to delete an invoice. And time entries fire on creation only.
GET /webhooks/events returns the current catalogue, so read it from the API rather than copying a list into your own code.
All of these fire for writes made through API v3 only, not for work done in the Uku web app.
Subscribe with API v3
Section titled “Subscribe with API v3”One v3 subscription is one event going to one URL, so listening for three events means creating three subscriptions. Uku does not deduplicate them: two subscriptions with the same event and URL deliver everything twice, so check GET /webhooks before registering the same hook again.
-
Create an API v3 key with the All scope. Every v3 webhook endpoint needs one — the
adminscope in the API documentation. A Read or Edit key cannot even list subscriptions. -
Send the subscription request, naming one event and the URL it should reach:
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"}' -
Store the signing secret that comes back. Uku shows it only this once, so keep it as carefully as the API key itself.
-
Map your receiver’s fields from
GET /webhooks/example, which returns a delivery in its real shape. Mapping from that beats waiting for a real event to arrive.
If you lose the secret, POST /webhooks/{id}/rotate-secret mints a new one. The old secret stops working the moment you do, with no overlap period, so update your receiver at the same time.
The v3 engine is stricter about your URL than v1.0 is. It checks the host when you subscribe, validates it again at delivery time, connects to the exact address it validated, and refuses to follow redirects. A URL that resolves to a private, loopback, link-local or reserved address is rejected with a 400 at subscribe time rather than failing silently later.
What an API v3 delivery looks like
Section titled “What an API v3 delivery looks like”A v3 delivery is a POST whose body wraps the record in an envelope carrying the event name and when it happened. 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.createdX-Uku-Delivery-Id: 8f1c2a4e-3b7d-4c21-9f0e-2a6b5d8c1e73X-Uku-Webhook-Id: 42X-Uku-Signature: sha256=<hex>X-Uku-Timestamp: <unix seconds>X-Uku-Retry-Num: <n> (only on retries)The request also identifies itself as User-Agent: Uku-Webhooks/3.0. If your receiver sits behind a web application firewall, that is the string to allow.
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. occurred_at is the event’s own time, not the send time — a delivery retried half an hour later still reports when the change actually happened.
Verifying an API v3 signature
Section titled “Verifying an API v3 signature”Every v3 delivery is signed with that subscription’s own secret, so your receiver can prove the call came from Uku for your subscription. A single shared key can never establish that. 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, because re-serialising changes them. Check the timestamp and the digest: a valid signature stays valid forever, and only the clock check stops a captured delivery from being replayed at you. Reject deliveries more than five minutes old.
Uku will not send a v3 delivery it cannot sign. If a subscription somehow has no secret, the delivery is refused and logged rather than sent unsigned.
Retries, failures, and replays in API v3
Section titled “Retries, failures, and replays in API v3”A failed v3 delivery is retried on a fixed ladder, and Uku eventually gives up on a receiver that stays broken. Answer with any 2xx status within 10 seconds. Acknowledge first and do your processing afterwards, because 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. Each of those means the request will never succeed, so Uku gives up immediately. Return
410 Goneto say “this receiver is retired, stop sending.” - Auto-disable. After 10 consecutive failures the subscription is switched off (
is_activegoes false, andfailure_count,last_failure_at, anddisabled_attell you why). Uku emails and notifies the Company Owner and whoever created the subscription, so you find out without polling. Fix your endpoint, then re-enable withPATCH /webhooks/{id}and{"is_active": true}— that also clears the failure count. - History and replay.
GET /webhooks/{id}/deliverieslists attempts with their response status — one row per attempt, so a retried event appears several times. Each row also stores what your endpoint sent back, up to a size limit. You can read the error your receiver returned without opening your own logs. To send one again, take itsevent_idand callPOST /webhooks/{id}/deliveries/{event_id}/replay. A delivery still working through its retries returns409 DELIVERY_IN_PROGRESS, so let it finish first. - Pacing is not failure. During a bulk operation, such as importing a thousand clients, Uku spaces deliveries out rather than firing them all at once. Some then arrive a few minutes late. A paced delivery is deferred, never failed. Pacing only starts far above the volume normal traffic produces, it does not consume retries, and it never counts toward the auto-disable limit. Do not mistake it for your endpoint being down. A delivery that stays deferred for too long is parked with a “throttled for too long” status rather than dropped. You can still replay it.
Subscribe on the legacy API v1.0
Section titled “Subscribe on the legacy API v1.0”Use this engine only to keep an existing integration running. For anything new, subscribe with API v3.
The v1.0 trigger is narrow: a client or a contact created in the Uku web app, through client_added and contact_added. There is no update or delete trigger on this engine, and nothing else fires it. Nothing else produces a v1.0 delivery: not a record created through API v1.0 itself, nor through API v3, an import, an automation, a scheduled job, or the Client Portal. So an integration that writes into Uku never hears its own writes come back.
Authenticate the way the rest of API v1.0 works: exchange your key pair for a JWT token, then send that token. The token is only valid for ten minutes, so a script that subscribes and then goes quiet has to log in again before its next call.
curl -X POST https://app.getuku.com/api/v1.0/webhooks/subscription \ -H "Authorization: Bearer <your-jwt>" \ -H "Content-Type: application/json" \ -d '{"webhook": "client_added", "url": "https://example.com/hooks/uku"}'To stop receiving them, DELETE /api/v1.0/webhooks/subscription/{id}. Creating and deleting a subscription is the entire management surface: there is no pause, no edit, and no way to see past deliveries.
What an API v1.0 delivery looks like
Section titled “What an API v1.0 delivery looks like”A v1.0 delivery is a POST whose body is the created record itself, flat, with no envelope around it. For a client that is the same shape the v1.0 GET /clients endpoint returns. For a contact it is the contact record with the client it belongs to carried as client_id. created_at and updated_at come through in ISO form. The example below is abridged — a real client record carries many more fields than these.
{ "id": 123, "name": "Apple Ltd", "reg_code": "12345678", "created_at": "2026-08-13T09:41:07Z", "updated_at": "2026-08-13T09:41:07Z"}Only two headers come with it: Content-Type, and — when the platform signing key is configured — X-Webhook-Signature, an HMAC-SHA256 hex digest of the raw body. There is no event-name header on this engine, so one URL receiving both client_added and contact_added cannot tell them apart without inspecting the body. Give each v1.0 event its own URL and the problem disappears.
The v1.0 signing key is a single Uku-wide value rather than one issued per subscription, and Uku’s interface does not surface it anywhere. So you cannot set up signature verification yourself on this engine. Per-subscription signing is a v3 feature. On v1.0 the safer pattern is to treat a delivery as a notification that something was created, then read the record back from the API before acting on it. Putting an unguessable token in your receiving URL’s path is a reasonable extra check.
Your URL has to be a public internet address, and on this engine the check happens at delivery time rather than when you subscribe. A private or loopback address is accepted as a subscription and then quietly fails every delivery. See Why does Uku say my webhook URL is invalid?.
Retries on API v1.0
Section titled “Retries on API v1.0”A v1.0 delivery that fails is retried a small number of times, seconds apart, and then dropped. Answer promptly: the timeout is short, and a slow endpoint reads as a failed one.
Nothing about that is visible to you afterwards. This engine records no delivery history, so a failed delivery leaves no row you can inspect or replay. A subscription pointing at a dead endpoint keeps being tried indefinitely rather than switching itself off. If a v1.0 webhook matters to your process, reconcile periodically by reading the records from the API rather than relying on delivery alone.
Moving from API v1.0 to API v3
Section titled “Moving from API v1.0 to API v3”Moving an existing webhook integration to v3 is a rebuild of the receiver rather than a setting you flip. Three of the changes are easy to miss, so plan for them before you start.
Running both engines at once double-delivers. A v1.0 subscription and a v3 subscription covering the same thing both fire, so one client creation produces two deliveries in two different shapes. The overlap is useful while you verify the new receiver, and a bug if you leave it in place. Delete the v1.0 subscription with DELETE /api/v1.0/webhooks/subscription/{id} once v3 deliveries are landing correctly.
The event names change. client_added becomes client.created, and contact_added becomes contact.created. There is no translation layer between the two vocabularies, and a v1.0 subscription is never rewritten into a v3 one.
What you hear about changes, not just how it looks. v1.0 fires only for records created in the Uku web app. v3 fires only for writes made through API v3, which includes anything built on Uku’s MCP server, since it calls that same API. So a firm whose clients are entered by staff in the browser sees no v3 deliveries for them. An integration that creates clients through the API, and has therefore never produced a single v1.0 delivery, produces v3 ones from its first write. Work out which of those two describes your firm before you assume the move is like-for-like.
Two smaller differences are worth wiring in at the same time. v3 sends an X-Uku-Event header, so one URL can serve several events. And it identifies itself as User-Agent: Uku-Webhooks/3.0, which a firewall in front of your receiver may need to allow.
Where Uku shows your webhooks
Section titled “Where Uku shows your webhooks”Uku’s webhook monitoring panel lists API v3 subscriptions only. A v1.0 subscription never appears there, however well it is delivering, so an empty panel is not evidence that anything has stopped.
Path: Settings & Apps → Public API → Public API → Connected webhooks
Each row gives the subscription’s Event, URL, Status, Failures, and Created date. Active means deliveries are landing. Failing (N) means the last N in a row failed, and hovering the count shows when the last one was. Disabled means the subscription is not active — either you paused it, or ten consecutive failures switched it off, and the Failures count is how you tell which. Each row has a Pause, Re-enable or Delete action. Expanding a row shows recent deliveries with the status your endpoint returned, which attempt it was, and a Replay button on the ones that failed.
Troubleshooting
Section titled “Troubleshooting”Why is my webhook subscription not in Connected webhooks?
Section titled “Why is my webhook subscription not in Connected webhooks?”The Connected webhooks panel only lists API v3 subscriptions. A webhook you set up earlier through Zapier, Make or your own code is most likely a v1.0 subscription, and those never appear there. To check one, call GET /api/v1.0/webhooks/subscription, which lists what actually exists.
Why did my webhook stop receiving anything?
Section titled “Why did my webhook stop receiving anything?”On API v3, a subscription that goes quiet has usually been auto-disabled. Open Settings & Apps → Public API → Public API → Connected webhooks, check the Failures count next to a Disabled status, fix the endpoint, then click Re-enable. Deliveries that were already recorded can be replayed. Events fired while it was off were never recorded and cannot be, so read those records from the API to catch up.
On API v1.0, a subscription is never switched off automatically, so silence means one of three things. Nobody has created a client or contact in the Uku web app since you last looked. The record was created somewhere the v1.0 engine does not watch. Or your endpoint is failing. This engine keeps no delivery history, so there is nothing in Uku to inspect. Check your own receiver’s logs, confirm the endpoint answers a POST promptly, and confirm the subscription still exists with GET /api/v1.0/webhooks/subscription.
Why does Uku say my webhook URL is invalid?
Section titled “Why does Uku say my webhook URL is invalid?”Uku only delivers to public internet addresses, on both engines. 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 will not be delivered to. API v3 rejects such a URL outright with a 400 when you create the subscription. API v1.0 accepts it and then fails silently on every delivery, which looks identical to an endpoint that is down. For local development, put a tunnelling service in front of your machine so the URL is publicly reachable.
Why does my receiver get the same event twice?
Section titled “Why does my receiver get the same event twice?”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. On API v3, every copy of an event carries the same id in the body. Treat that as the key and ignore an id you have already processed. On API v1.0 there is no such id, so deduplicate on the record’s own id together with created_at. If you are running both engines during a migration, the duplicate is more likely to be one delivery from each engine than a retry.
Does using API v3 webhooks break my existing Zapier or Make connection?
Section titled “Does using API v3 webhooks break my existing Zapier or Make connection?”No. The two engines are kept apart in the database, and each delivers only to its own subscriptions. A subscription created through v1.0 keeps receiving the flat v1.0 body and the X-Webhook-Signature header. If you want the richer v3 event set, create a new v3 subscription — the old one is not migrated or rewritten. While both exist you receive each event twice, which is the point to plan for.