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
- Register one HTTPS endpoint for your environment with your Bearer token.
- Choose all events (omit
event_types, use[],"*", or["*"]) or
an explicit list from the event catalog. - FikaChu stores each event in a transactional outbox and delivers it to every
active endpoint owned by your service-account user. - Verify the signature before parsing the body.
- Deduplicate on the event ID. Delivery is at-least-once.
- 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
PATCHto 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-Afteris honoured, capped at one hour.- Redirects are not followed.
- After the retry window the delivery is marked
deadand can be replayed.
At-least-once and ordering
- Delivery is at-least-once. Duplicates are possible.
- Ordering is not guaranteed. Use
occurred_atandprevious_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 bodyid).
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_countandreplay_countnext_attempt_atlast_attempt_atlast_response_statuslast_errorlast_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
- Register a staging endpoint with
event_types: "*"(or["*"]). - Send
POST .../testand verify the signature on thewebhook.testevent. - Create a staging ride and confirm
ride.createdandride.status_changed. - Confirm duplicate events are ignored by event ID.
- For delivery flows, confirm
ride.stop_completedincludes verification
summaries. - Simulate a 500 response and verify FikaChu retries; then inspect the delivery
history and replay it. - Rotate the secret and confirm your receiver accepts both secrets during the
grace period. - 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.