Kilo Docs

Docs / Webhooks/ Webhooks

Webhooks

Updated Aug 17, 2026

When something important happens, Kilo POSTs JSON to your HTTPS URL.

Why webhooks feel “faster than the HTTP response”

Your POST /deliveries returns as soon as Kilo saves the job.

Separately, we enqueue the webhook with Upstash QStash, so:

  • Your create API stays fast.
  • Your webhook URL is called with retries if it is down.
  • You still verify that the POST really came from Kilo.

Create an endpoint

  1. Dashboard → Settings → API & Webhooks.
  2. Add endpoint with an https:// URL.
  3. Choose events (start with all delivery events).
  4. Copy the whsec_... signing secret (shown once).

Events

Event When (plain English)
delivery.created A new delivery was saved
delivery.cancelled Delivery cancelled
delivery.status_changed Status moved (rider assigned, picked up, …)
delivery.updated General update
delivery.completed Delivery finished
delivery.otp_issued The delivery code is ready for you to send (only when OTP-via-webhook is on)

Assign a rider from delivery.created

If Auto-dispatch is off, jobs stay pending until someone assigns a rider. Your webhook handler can:

  1. Read data.object (id, pickup lat/lng, rider_assigned).
  2. Skip if rider_assigned is already true.
  3. GET /riders?pickup_lat=…&pickup_lng=… — this ranks by distance. It does not hide busy riders. Pending invites never appear.
  4. Pick a rider (on_delivery is OK — same as the dashboard). Prefer smallest distance_km.
  5. POST /deliveries/{id}/assign with { "riderId": "…" }.

Then you will get delivery.status_changed with status matched. Full walkthrough: Assign riders. Copy-ready prompt: Auto-assign agent.

Headers on every webhook

Header Meaning
Content-Type application/json
Kilo-Signature t=<unix>,v1=<hex>
Kilo-Event Event name

Need the full webhook body shape? Jump to API reference → Webhook POST.

Verify the signature (do this for real)

  1. Read Kilo-Signature.
  2. Split into t and v1.
  3. Reject if t is older than 5 minutes.
  4. Compute HMAC-SHA256 of ${t}.${rawBody} using your whsec_... secret.
  5. Compare hex digests in constant time.

Example Node.js:

JavaScript
import crypto from "node:crypto";

function verify(secret, rawBody, header) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => {
      const [k, ...rest] = p.split("=");
      return [k, rest.join("=")];
    }),
  );
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Respond

Return HTTP 2xx quickly. Do heavy work in your own queue after verifying the signature.

Handle customer messages yourself

Many bots already own the conversation with the customer. In that case:

  1. Dashboard → Settings → Notifications → turn Customer delivery messages off.
  2. Subscribe to the events above (especially delivery.status_changed / delivery.completed).
  3. Send WhatsApp / SMS / push from your product when those webhooks arrive — use the summary / customer_message_hint / agent_notes fields on each webhook body (plus docs_url) so an AI agent knows what to tell the customer without guessing.

Kilo will not send tracking links, pickup/nearby/arrived updates, or completion/feedback WhatsApps to your customers while that setting is off. By default, delivery OTPs are still sent by Kilo (WhatsApp + SMS) after the fee is marked paid — unless the fleet turned Require OTP at drop-off off under Delivery settings (photo proof only; no code is issued). Rider notifications still work.

Partner API creates use the same path as the public request form for tracking messages, so the toggle applies to API-created jobs too.

Send the delivery OTP yourself

Why this exists

Sometimes Kilo cannot reliably reach the customer on WhatsApp (Meta rules, 24‑hour window, template approval, etc.). If you already chat with the customer in your bot, you can deliver the code yourself.

When there is no OTP at all

Dashboard → Settings → Delivery → Require OTP at drop-off is on by default. If your fleet turns it off, riders finish drop-off (and returns) with photo proof only — Kilo will not WhatsApp/SMS a code, will not fire delivery.otp_issued, and Resend delivery OTP is hidden. Use that when speed matters more than a customer code. New jobs snapshot the setting; older jobs keep whatever they were created with.

Baby steps (do these in order)

  1. Open the business dashboard → Settings → API & Webhooks.
  2. Add a webhook endpoint if you do not have one yet (must be https://).
  3. Make sure the endpoint listens for delivery.otp_issued (or all events / *).
  4. Confirm Require OTP at drop-off is still on (Delivery settings) — otherwise there is nothing to send.
  5. Turn OTP via webhook only ON.
  6. From that moment:
    • When staff marks the delivery fee as paid, Kilo does not WhatsApp/SMS the OTP.
    • Instead Kilo POSTs delivery.otp_issued to your URL with the code.
    • Dashboard Resend delivery OTP does the same thing (still needs fee paid + dropoff statuses; no cool-off while OTP via webhook is on).

What the OTP webhook looks like

JSON
{
  "id": "delivery.otp_issued_DEL_…_…",
  "type": "delivery.otp_issued",
  "created": 1710000000,
  "data": {
    "object": { "id": "DEL_…", "status": "…", "tracking": { "url": "…" } },
    "previous_status": null,
    "otp": {
      "code": "4821",
      "receiver_phone": "+2348012345678",
      "reason": "payment",
      "channel": "webhook"
    }
  },
  "summary": "Delivery OTP for DEL_… was sent after the fee was marked paid. …",
  "customer_message_hint": "Your delivery code for DEL_… is ready. …",
  "docs_url": "https://docs.kiloapp.org/webhooks",
  "agent_notes": ["Read data.otp.code …"]
}
Field Meaning
data.otp.code The 4‑digit (or similar) code the receiver gives the rider
data.otp.receiver_phone Who should get it
data.otp.reason payment (first send after mark paid) or resend
data.otp.channel Always webhook for this event

Rules (please do not skip)

  • Never invent an OTP. Only use data.otp.code from this event.
  • Never put the OTP in a public group. Send it only to the customer.
  • If OTP-via-webhook is off, Kilo keeps sending WhatsApp + SMS itself (old behaviour).
  • Turning Customer delivery messages off does not by itself stop OTP — you need the OTP-via-webhook switch for that (or turn Require OTP at drop-off off under Delivery settings).
  • If Require OTP at drop-off is off, expect neither Kilo SMS/WhatsApp nor delivery.otp_issued.