Webhooks
Sparro sends a signed HTTPS request to your endpoint when something changes for a company connected to your app. Webhooks are available on Partner and Platform. You create an endpoint and see its signing secret in the developer portal.
Events
| Event | Sent when |
|---|---|
shift.created | A shift or hours entry is created through the API (POST /shifts or POST /shifts/hours). Sent to apps with the shifts:read scope for that company. |
mileage.created | A mileage entry is created through the API (POST /mileage). Sent to apps with the mileage:read scope for that company. |
connection.revoked | The company removes your app. This is the last event you get for that company; delete what you hold on it that you no longer need. |
webhook.test | You press Send test event on an endpoint in the developer portal. It carries no company and an empty data object. |
shift.createdmileage.createdconnection.revokedwebhook.testNew event types and new fields can appear without notice. Ignore the ones you do not know.
Delivery
Each event is a POST with a JSON body. Respond with any 2xx within 10 seconds. Anything else is retried, after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours. After the last retry the delivery is given up; after 5 deliveries in a row have used all their retries, the endpoint is disabled with a reason, and you turn it back on in the portal. The test event is sent once and never retried.
POST /hooks/sparro HTTP/1.1
Sparro-Signature: t=<unix seconds>,v1=<hex>
Sparro-Event-Id: <event id>
Sparro-Delivery-Id: <delivery id>
Content-Type: application/json
{ "id": "<event id>", "type": "shift.created", "created": "<ISO 8601>",
"data": { "org_id": "…", "shift": { "id": "…", "employee_ref": "emp_…", "start_time": "…", "end_time": "…", "hours": 7.5 } } }Verify the signature
Compute an HMAC-SHA256 of t + "." + raw_body with your endpoint's signing secret and compare it with v1 in constant time. Use the body exactly as it arrived, before any JSON parsing. Reject events whose t is more than 5 minutes from your clock.
t, v1 = parse(request.headers["Sparro-Signature"])
signed = t + "." + request.raw_body
expect = hex(HMAC_SHA256(signing_secret, signed))
valid = constant_time_equal(expect, v1)
and abs(now() - t) <= 300Run this test vector through your verifier before you go live. It uses a fixed fake secret that signs nothing real.
secret = "whsec_test_vector_not_a_real_secret"
t = 1792341127
raw_body = {"id":"7b5a1f0e-3c2d-4e8a-9b61-0d4f2a8c9e11","type":"webhook.test","data":{}}
signature = t=1792341127,v1=8e09ed0e7905e2cac6d751fb9e1c7f90ccb0b55f58703146387aed666bab5192Replay and duplicates
Every delivery of one event carries the same event id: in the body as id and in the Sparro-Event-Id header. Retries and replays send it again, so store the ids you have handled and ignore repeats. The Sparro-Delivery-Id header identifies one delivery; a replay from the portal is a new delivery of the same event. Missed events stay in the delivery log for 30 days and can be replayed for the first 7.