FikaChu Docs

FikaChu Webhooks

Webhooks let your platform receive signed, durable notifications from
FikaChu instead of polling every ride. This guide covers endpoint registration,
event selection, signature verification, delivery behaviour, replay, and common
integration problems.

Audience: third-party ride-booker, delivery, corporate-travel, and vehicle
rental partners.

For booking, estimates, and ride lifecycle endpoints, see the
Ride Booker API guide.


1. How it works

  1. Register one HTTPS endpoint for your environment with your Bearer token.
  2. Choose all events (omit event_types, use [], "*", or ["*"]) or
    an explicit list from the event catalog.
  3. FikaChu stores each event in a transactional outbox and delivers it to every
    active endpoint owned by your service-account user.
  4. Verify the signature before parsing the body.
  5. Deduplicate on the event ID. Delivery is at-least-once.
  6. Use delivery history/replay or GET /rides?updated_since=... to recover from
    missed or failed deliveries.

Webhooks are registered per environment, not per ride. The old
create-time webhook_url field has been removed.


2. Authentication

All webhook management endpoints use the same OIDC/OAuth access token as
the rest of the partner API:

Authorization: Bearer <access_token>

Endpoints are owned by the authenticated User. One service account per
environment is recommended.


3. Register an endpoint

POST /ride-booker/v1/webhooks/endpoints
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "url": "https://shop.example.com/hooks/fikachu",
  "description": "Production order updates",
  "event_types": ["*"]
}

Event selection

event_types is optional.

Value Meaning
omitted All event types
[] All event types
"*" All event types
["*"] All event types
["ride.status_changed", "ride.stop_completed"] Only those event types

If "*" is present, the endpoint is treated as subscribed to every event type,
even if other names are also supplied. The API canonicalizes this to ["*"].

Use "*", ["*"], or omit the field when you want future event types to be
delivered without reconfiguring the endpoint. Use an explicit list only when
your receiver intentionally ignores other event types.

Response

The response includes the signing secret once:

{
  "id": "7d0f6d20-1e0e-4c6b-8c4e-2f1c6e1a2b3c",
  "url": "https://shop.example.com/hooks/fikachu",
  "description": "Production order updates",
  "event_types": ["*"],
  "api_version": "2026-10-22",
  "is_active": true,
  "secret": "whsec_...",
  "secret_last4": "9f2a",
  "created": "2026-10-21T12:00:00Z",
  "updated": "2026-10-21T12:00:00Z"
}

Store the secret in a secret manager. It is not returned by GET again.

Endpoint limits and URL requirements

  • Up to 10 active endpoints per user.
  • URL must be https:// in production, port 443, public internet host, no
    userinfo, no fragment, and at most 2048 characters.
  • FikaChu blocks loopback, private, link-local, reserved, and cloud-metadata
    addresses, resolves DNS before delivery, and does not follow redirects.
  • A URL can only be registered once per user. Use PATCH to change it.

4. Manage endpoints

Method Path Purpose
GET /ride-booker/v1/webhooks/endpoints List your endpoints
POST /ride-booker/v1/webhooks/endpoints Create an endpoint; returns the secret once
GET /ride-booker/v1/webhooks/endpoints/{id} Read one endpoint
PATCH /ride-booker/v1/webhooks/endpoints/{id} Change URL, description, event types, or active state
DELETE /ride-booker/v1/webhooks/endpoints/{id} Soft-disable; history is retained
POST /ride-booker/v1/webhooks/endpoints/{id}/rotate-secret Rotate the signing secret
POST /ride-booker/v1/webhooks/endpoints/{id}/test Send a signed webhook.test
GET /ride-booker/v1/webhooks/endpoints/{id}/deliveries Delivery history
GET /ride-booker/v1/webhooks/endpoints/{id}/deliveries/{delivery_id} Attempt detail
POST /ride-booker/v1/webhooks/endpoints/{id}/deliveries/{delivery_id}/replay Re-attempt an event
GET /ride-booker/v1/webhooks/event-types Event catalog

Disable and re-enable

DELETE is a soft disable. The endpoint and its delivery history remain.

PATCH /ride-booker/v1/webhooks/endpoints/{id}
{ "is_active": false }

Re-enable with:

PATCH /ride-booker/v1/webhooks/endpoints/{id}
{ "is_active": true }

FikaChu also pauses an endpoint after repeated failures and auto-disables it
after a sustained failure window. Re-enable it with PATCH { "is_active": true }
after fixing your receiver.

Send a test event

POST /ride-booker/v1/webhooks/endpoints/{id}/test

This returns 202 and a delivery record. The test event is signed exactly like
production events and has type: "webhook.test".


5. Event catalog

Fetch the current catalog from GET /ride-booker/v1/webhooks/event-types, or use
this table. The catalog includes the "*" wildcard row. The wildcard subscribes
to every type, including future types.

Event type When it is emitted Key data fields
ride.created A ride or rental is created data.resource, external_id, metadata, status, booking type, stops, manifest summary
ride.status_changed A persisted ride status transition status, previous_status, occurred_at, driver/vehicle summary
ride.stop_completed A driver confirms a pickup, mid-stop, dropoff, return, or final stop stop object, is_final_stop, verification summary, driver_note
ride.payment_updated Rider-side invoice/payment status changes payment_status, invoice_ref, amount
rental.session_changed Rental pause, resume, or stopping intent is_paused, pause_deadline, is_stopping
webhook.test Requested via the test endpoint data.resource (webhook endpoint), challenge

Every payload identifies the affected entity exactly once:

"resource": { "type": "ride", "id": "550e8400-e29b-41d4-a716-446655440000" }

There is no separate data.ride_id / data.<resource>_id field. Use
data.resource.type and data.resource.id.

ride.created

{
  "id": "3f2b8a4e-1c2d-4a5b-9c6d-7e8f9a0b1c2d",
  "type": "ride.created",
  "api_version": "2026-10-22",
  "created_at": "2026-10-21T12:00:00Z",
  "data": {
    "resource": { "type": "ride", "id": "550e8400-e29b-41d4-a716-446655440000" },
    "external_id": "shop-order-778",
    "metadata": { "shop_id": "acme" },
    "status": "SEARCHING",
    "booking_type": "on_demand",
    "service_type": "delivery",
    "details_url": "https://api.example.com/ride-booker/v1/rides/details?ride_id=550e8400-...",
    "created": "2026-10-21T12:00:00Z",
    "stops": [
      {
        "order": 0,
        "address": "Pickup",
        "lat": -6.78652,
        "lon": 39.259711,
        "notes": "Collect both catalog items",
        "completed_at": null,
        "verification": {
          "pincode": false,
          "picture": false,
          "signature": false,
          "barcodes": []
        }
      }
    ],
    "manifest_items": [
      { "external_id": "BOX-1", "name": "Parcel box", "quantity": 1.0 }
    ],
    "estimated_cost": "12000.00",
    "total_cost": null,
    "currency": "TZS"
  }
}

ride.status_changed

{
  "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "type": "ride.status_changed",
  "api_version": "2026-10-22",
  "created_at": "2026-10-21T12:00:00Z",
  "data": {
    "resource": { "type": "ride", "id": "550e8400-e29b-41d4-a716-446655440000" },
    "external_id": "shop-order-778",
    "metadata": { "shop_id": "acme" },
    "status": "DRIVER_ASSIGNED",
    "previous_status": "SEARCHING",
    "occurred_at": "2026-10-21T12:00:00Z",
    "service_type": "delivery",
    "booking_type": "on_demand",
    "driver": { "id": 42, "name": "Asha Mwangi" },
    "vehicle": { "vehicle_id": "bike-001", "plate_no": "TZ-001", "vehicle_type": "Electric Scooter" },
    "total_cost": null,
    "currency": "TZS",
    "start_time": null,
    "end_time": null,
    "details_url": "https://api.example.com/ride-booker/v1/rides/details?ride_id=550e8400-..."
  }
}

Ride statuses include REQUESTED, SEARCHING, DRIVER_ASSIGNED,
DRIVER_ON_ROUTE_TO_CLIENT, ARRIVED_AT_CLIENT, DRIVING_WITH_CLIENT,
COMPLETED, CANCELLED, CLIENT_CANCELLED, CLIENT_DID_NOT_SHOW,
NO_DRIVER_FOUND, PAYMENT_BOOKING_FAILED, SCHEDULED, DRIVER_COMMITTED,
READY_TO_DISPATCH, DISPATCHED, and OTHER.

ride.stop_completed

{
  "id": "7e6d5c4b-3a21-4f0e-9d8c-7b6a5f4e3d2c",
  "type": "ride.stop_completed",
  "api_version": "2026-10-22",
  "created_at": "2026-10-21T12:05:00Z",
  "data": {
    "resource": { "type": "ride", "id": "550e8400-e29b-41d4-a716-446655440000" },
    "external_id": "shop-order-778",
    "stop": {
      "order": 1,
      "address": "Dropoff",
      "lat": -6.798702,
      "lon": 39.255937,
      "notes": "",
      "completed_at": "2026-10-21T12:05:00Z",
      "verification": {
        "pincode": true,
        "picture": true,
        "signature": false,
        "barcodes": ["BOX-1"]
      }
    },
    "is_final_stop": true,
    "current_stop_index": 1,
    "driver_note": "Handed to reception",
    "occurred_at": "2026-10-21T12:05:00Z",
    "details_url": "https://api.example.com/ride-booker/v1/rides/details?ride_id=550e8400-..."
  }
}

Media files are not embedded in webhook payloads. Use details_url if you need
the full ride detail, subject to your normal API access.

ride.payment_updated

{
  "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "type": "ride.payment_updated",
  "api_version": "2026-10-22",
  "created_at": "2026-10-21T12:10:00Z",
  "data": {
    "resource": { "type": "ride", "id": "550e8400-e29b-41d4-a716-446655440000" },
    "external_id": "shop-order-778",
    "payment_status": "paid",
    "invoice_ref": "inv-123",
    "amount": "12000.00",
    "currency": "TZS",
    "occurred_at": "2026-10-21T12:10:00Z",
    "details_url": "https://api.example.com/ride-booker/v1/rides/details?ride_id=550e8400-..."
  }
}

rental.session_changed

{
  "id": "8f7e6d5c-4b3a-2f1e-0d9c-8b7a6f5e4d3c",
  "type": "rental.session_changed",
  "api_version": "2026-10-22",
  "created_at": "2026-10-21T12:15:00Z",
  "data": {
    "resource": { "type": "ride", "id": "550e8400-e29b-41d4-a716-446655440000" },
    "status": "DRIVING_WITH_CLIENT",
    "is_paused": true,
    "pause_deadline": "2026-10-21T12:30:00Z",
    "is_stopping": false,
    "occurred_at": "2026-10-21T12:15:00Z"
  }
}

webhook.test

{
  "id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
  "type": "webhook.test",
  "api_version": "2026-10-22",
  "created_at": "2026-10-22T12:20:00Z",
  "data": {
    "resource": { "type": "webhook_endpoint", "id": "7d0f6d20-1e0e-4c6b-8c4e-2f1c6e1a2b3c" },
    "message": "FikaChu webhook test event.",
    "challenge": "d4f2..."
  }
}

6. Verify the signature

Every delivery includes:

Content-Type: application/json
User-Agent: FikaChu-Webhooks/1.0
Fikachu-Event-Id: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Fikachu-Event-Type: ride.status_changed
Fikachu-Delivery-Id: 2a1f...
Fikachu-Api-Version: 2026-10-22
Fikachu-Signature: t=1722686400,v1=<hex hmac-sha256>

The signed content is:

<timestamp>.<raw request body>

Use HMAC-SHA256 with your endpoint secret. Reject timestamps more than 300
seconds
away from your clock. Verify before parsing or re-serializing the body.

Python

import hashlib
import hmac
import time


def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp = int(parts["t"])
    if abs(int(time.time()) - timestamp) > tolerance:
        return False
    expected = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}.".encode("utf-8") + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Node.js / Express

const crypto = require("crypto");

function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((item) => item.split("=", 2))
  );
  const timestamp = Number(parts.t);
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > tolerance) {
    return false;
  }
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  const provided = Buffer.from(parts.v1, "hex");
  const actual = Buffer.from(expected, "hex");
  return (
    provided.length === actual.length &&
    crypto.timingSafeEqual(provided, actual)
  );
}

// Use express.raw({ type: "application/json" }) so rawBody is the exact bytes.

Secret rotation

POST /ride-booker/v1/webhooks/endpoints/{id}/rotate-secret

The response contains the new secret. The previous secret remains valid for
24 hours, so deploy the new secret to your receiver and accept both during
that window.

Never put the secret in the webhook URL. Keep it in a secret manager and never
log it.


7. Delivery behaviour

Response requirements

  • Return any 2xx status within 5 seconds.
  • Body is ignored; return {} or a short message.
  • Do not perform slow downstream work synchronously. Enqueue the event and return
    2xx.

Retries

Transient failures are retried with jitter:

1m, 5m, 30m, 2h, 6h, 12h, 24h
  • Retried: 408, 425, 429, 500, 502, 503, 504, timeouts, connection
    errors, and temporary DNS failures.
  • Not retried: 3xx, 400, 401, 403, 404, 405, 410, 422, and
    permanent URL validation failures.
  • Retry-After is honoured, capped at one hour.
  • Redirects are not followed.
  • After the retry window the delivery is marked dead and can be replayed.

At-least-once and ordering

  • Delivery is at-least-once. Duplicates are possible.
  • Ordering is not guaranteed. Use occurred_at and previous_status.
  • Never regress out of a terminal state: COMPLETED, CANCELLED,
    CLIENT_CANCELLED, CLIENT_DID_NOT_SHOW, NO_DRIVER_FOUND,
    PAYMENT_BOOKING_FAILED.
  • Deduplicate on Fikachu-Event-Id (same value as body id).

Failure protection

  • Repeated failures pause the endpoint briefly.
  • A sustained failure window auto-disables the endpoint.
  • Re-enable with PATCH { "is_active": true } after fixing the receiver.
  • Events and deliveries are retained for 30 days for replay.

8. Delivery history and replay

List deliveries:

GET /ride-booker/v1/webhooks/endpoints/{id}/deliveries?status=dead&event_type=ride.status_changed

Delivery statuses: pending, queued, in_progress, succeeded, dead.

Delivery fields include:

  • attempt_count and replay_count
  • next_attempt_at
  • last_attempt_at
  • last_response_status
  • last_error
  • last_duration_ms
  • event ID/type/resource and occurred_at

Inspect one delivery and its attempts:

GET /ride-booker/v1/webhooks/endpoints/{id}/deliveries/{delivery_id}

Replay after fixing your endpoint:

POST /ride-booker/v1/webhooks/endpoints/{id}/deliveries/{delivery_id}/replay

Replay keeps the same event ID and body, generates a fresh signature timestamp,
and appends new attempts to the delivery history.


9. Reconciliation and idempotency

Webhooks can be missed. Use the API as the source of truth when needed:

GET /ride-booker/v1/rides?updated_since=2026-10-21T00:00:00Z&page=1
GET /ride-booker/v1/rides?external_id=shop-order-778

Send external_id on create to correlate rides without relying only on your own
storage. It is unique per caller when set.

Create endpoints also accept:

Idempotency-Key: <your stable request key>

Reusing the same key with the same body returns the original response and sets
Idempotent-Replayed: true. Reusing it with a different request returns
409 idempotency_key_reuse.


10. Sandbox and integration checklist

  1. Register a staging endpoint with event_types: "*" (or ["*"]).
  2. Send POST .../test and verify the signature on the webhook.test event.
  3. Create a staging ride and confirm ride.created and ride.status_changed.
  4. Confirm duplicate events are ignored by event ID.
  5. For delivery flows, confirm ride.stop_completed includes verification
    summaries.
  6. Simulate a 500 response and verify FikaChu retries; then inspect the delivery
    history and replay it.
  7. Rotate the secret and confirm your receiver accepts both secrets during the
    grace period.
  8. Move to production with a new endpoint and a new secret.

11. Troubleshooting

Symptom Likely cause Action
401 on endpoint API Missing/expired Bearer token Refresh the OIDC token
400 on endpoint create URL is HTTP, private, unresolvable, wrong port, or duplicate Use a public HTTPS URL on port 443
Endpoint receives nothing event_types excludes the event or endpoint is inactive/paused Set "*", ["*"], or the desired types; check is_active and paused_until
403 from webhook receiver Your edge rejects FikaChu Allowlist FikaChu egress IPs or remove the edge rule
Signature fails Body was parsed/re-serialized, wrong secret, or stale timestamp Verify raw bytes, compare secrets, check clock sync
Duplicate order updates At-least-once delivery Deduplicate on event ID
Status moves backwards Out-of-order delivery Use occurred_at/previous_status and terminal-state rules
Delivery dead Retry window exhausted Fix receiver, inspect attempt detail, replay
Endpoint auto-disabled Sustained failures Fix receiver, PATCH { "is_active": true }

For support, include the endpoint ID, delivery ID, event ID, UTC timestamp, HTTP
status, and response body from the delivery attempt.