Skip to content
OkadaDrop

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.

Webhook events and when each fires
delivery.acceptedFires whenA rider took the job. The payload names them.
delivery.picked_upFires whenThe parcel is on the bike.
delivery.in_transitFires whenThe rider is heading to the drop-off.
delivery.deliveredFires whenHanded over. The one most integrations act on.
delivery.cancelledFires whenThe delivery was called off.
delivery.expiredFires 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.

Callback
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.

NextTrackingStatus only, or the rider's live position on your own map.