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:
| Connect | Good for | |
|---|---|---|
| WebSocket | You open wss://…/v2/ws/partners/{partner_id} | Live maps and dashboards, local development, anything that's already a long-running client |
| Webhook | Serve POSTs to the webhook_url on your partner account | Backends 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
event.delivery_statusSent 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
event.courier_statusSent 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 productionThe 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)