Developers
Webhooks
Point OkadaDrop at an HTTPS endpoint on your server and every status change arrives there within a second of happening — signed, so you can prove it came from us.
Pointing OkadaDrop at your endpoint
Set the URL once, either from the dashboard or from the API. Setting it returns a signing secret — capture it from that response, because it is never shown again.
curl -X PUT https://api.okadadrop.com/api/v1/partner/webhook \
-H "Authorization: Bearer $SOMA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_url": "https://acme.example/hooks/okadadrop"}'
# The response carries the signing secret. This is the only time you see it.
{
"webhook_url": "https://acme.example/hooks/okadadrop",
"webhook_secret": "9f2a...c41e"
}Events
One event per status change, for deliveries booked on your account only.
| Event | Fires when |
|---|---|
| delivery.accepted | Fires whenA rider took the job. The payload names them. |
| delivery.picked_up | Fires whenThe parcel is on the bike. |
| delivery.in_transit | Fires whenThe rider is heading to the drop-off. |
| delivery.delivered | Fires whenHanded over. The one most integrations act on. |
| delivery.cancelled | Fires whenThe delivery was called off. |
| delivery.expired | Fires whenNo rider was found in time. |
What arrives
The envelope names the event and the delivery; data is the same full delivery object the REST API returns, so a webhook is usually enough on its own — you rarely need to call back for more.
POST https://acme.example/hooks/okadadrop
X-Soma-Signature: sha256=4c1f9b...
Content-Type: application/json
{
"event": "delivery.delivered",
"delivery_id": "8f3b2c1e-...",
"status": "delivered",
"occurred_at": "2026-07-30T12:34:56+00:00",
"data": {
"id": "8f3b2c1e-...",
"status": "delivered",
"total_fare": 26.36,
"recipient_name": "Ama"
}
}Verifying the signature
Every request carries X-Soma-Signature: an HMAC-SHA256 of the raw request body, keyed with your webhook secret. Recompute it over the exact bytes you received — not over a re-serialised object, whose key order and whitespace will differ — and compare in constant time.
import hashlib
import hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
"""Compare over the RAW bytes — re-serialising the JSON changes them."""
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)Handling them well
Answer fast
Return any 2xx as soon as you have the payload safely queued. Do your own work afterwards — a slow endpoint is treated as a failed one after 10 seconds.
Expect retries
A non-2xx or a timeout is retried with exponential backoff, up to five times. Your handler must be idempotent: the same event can legitimately arrive twice.
Verify every request
Check the X-Soma-Signature header before you trust a single field. An unsigned or wrongly-signed request is not from OkadaDrop, whatever it claims in the body.
Rotating the secret
Setting a new webhook URL issues a new signing secret and retires the old one immediately. Deploy the new secret before you change the URL.
# Answer first, work afterwards. A handler that does its
# processing before responding will be retried while it is still busy.
@app.post("/hooks/okadadrop")
async def okadadrop_webhook(request):
raw = await request.body()
if not verify(raw, request.headers.get("X-Soma-Signature"), SECRET):
return Response(status_code=401)
event = json.loads(raw)
# Idempotent: the same delivery_id + status may arrive more than once.
await queue.enqueue(event)
return Response(status_code=200)If you can’t receive callbacks
If your systems cannot accept an inbound request — no public endpoint, or a network that will not allow one — the REST API is enough on its own. Poll GET /partner/deliveries/{id} for the deliveries you have open. It costs you a request per delivery per check instead of a callback per change, so keep it to deliveries that are not yet in a terminal state.