Real-time events

Get delivery and courier updates pushed to you, over a WebSocket or to your webhook URL, instead of polling.

Polling GET /jobs/{job_id} works, but Serve can push the same changes to you as they happen. There are two channels, and both carry the same events in the same envelope:

ConnectGood for
WebSocketYou open wss://…/v2/ws/partners/{partner_id}Live maps and dashboards, local development, anything that's already a long-running client
WebhookServe POSTs to the webhook_url on your partner accountBackends that react to a status change without holding a connection open

The event envelope

{
  "created": "2026-10-29T04:01:59.969616Z",
  "event_id": "evt_FjIhj_thTyaD17C2EDnKLA",
  "kind": "event.courier_status",
  "sequence_id": null,
  "payload": { }
}

event_id is unique per event; a redelivered event carries the same event_id, so use it to discard duplicates.

event.delivery_status

Sent when a delivery's status changes. Drive your order state machine from this one.

{
  "delivery_id": "del_3vDjjkd21b",
  "status": "pickup_arrived",
  "pickup_eta": "2026-10-29T03:51:13Z",
  "dropoff_eta": "2026-10-29T03:57:56Z",
  "partner_metadata": null
}

event.courier_status

Sent on a timer while a courier is working, roughly every 5 seconds in sandbox. Use it for a live map, not to infer status changes: it fires on a clock, not on a transition.

{
  "courier_id": "ptnr_mE4qRi7wQVmpc6xlUDybzg.playground-1",
  "delivery_id": "del_3vDjjkd21b",
  "lat": 34.0227,
  "lng": -118.2798,
  "partner_metadata": null,
  "status": null
}

courier_id is your partner id and the robot's name, joined by a dot.

WebSocket

wss://api.sandbox.serverobotics.com/v2/ws/partners/{partner_id}

Authenticate with your API key, either as an X-API-Key header or, where you can't set headers (browsers), as an api_key query parameter. A connection without a valid key is accepted and then closed with code 1000.

const ws = new WebSocket(
  `wss://api.sandbox.serverobotics.com/v2/ws/partners/${PARTNER_ID}?api_key=${API_KEY}`
);
ws.onmessage = (msg) => {
  const event = JSON.parse(msg.data);
  if (event.kind === "event.delivery_status") updateOrder(event.payload);
  if (event.kind === "event.courier_status") moveMarker(event.payload);
};

The socket carries events for every courier and delivery on your partner, so filter by delivery_id or courier_id. Reconnect with backoff when the connection drops, and re-read GET /jobs/{job_id} after reconnecting to catch anything you missed.

🚧

Keep API keys out of browsers in production

The query-parameter form exists so a browser can connect, but a key in front-end code is a key anyone can read. In production, connect from your backend and relay to your clients.

Webhooks

Set webhook_url on your partner account (in Serve Connect, or ask your Serve representative) and Serve sends each event as a POST with the envelope above as the JSON body. Return a 2xx quickly and do the work asynchronously.

Verify the signature

Each request carries an X-SERVE-ROBOTICS-SIGNATURE header: the lowercase hex HMAC-SHA256 of the raw request body, keyed with the webhook secret on your partner account. There's no timestamp or prefix; the whole header value is the digest. Verify against the raw bytes before parsing.

const crypto = require("crypto");

function verify(rawBody, header, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return header && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)