Webhooks
SimplifyLocal POSTs a JSON event to your endpoint when a job’s page goes live, changes, or comes down. The payload says what changed, not what the new state is: fetch the job from the API, or invalidate your cache, in response.
Setting up an endpoint
Section titled “Setting up an endpoint”Email support@simplifylocal.app with the HTTPS URL to deliver to and the events you want. We register it and send you the signing secret once. Rotating the secret is the same request; the old secret stops being used as soon as the new one is set.
Events
Section titled “Events”| Event | Sent when |
|---|---|
job.published |
A job’s page goes live, whether automatically or by someone at the business. Also sent when a page that was unpublished goes live again, so handle it as an upsert. |
job.updated |
A live page changes: someone at the business edits it, changes its photos or reviews, or republishes it. |
job.unpublished |
A live page is taken down (moved back to draft or archived). The job now answers 404 job_not_found on the API. |
job.deleted |
Defined in the contract but not sent today; jobs are archived, not deleted, which arrives as job.unpublished. Handle it like job.unpublished so you are covered if it ships. |
Request
Section titled “Request”POST {your URL}Content-Type: application/jsonUser-Agent: SimplifyLocal-Webhook/1.0X-SimplifyLocal-Event: job.publishedX-SimplifyLocal-Delivery: 7a3f2c0e-5b1d-4e8a-9f6c-2d4b8e1a0c3fX-SimplifyLocal-Timestamp: 1715000000X-SimplifyLocal-Signature: sha256=5d41402abc4b2a76b9719d911017c592…| Header | Value |
|---|---|
X-SimplifyLocal-Event |
The event name, the same as event in the body. |
X-SimplifyLocal-Delivery |
A UUID unique to this delivery. The same value is sent again on every retry of the same delivery. |
X-SimplifyLocal-Timestamp |
Unix seconds when the request was signed. |
X-SimplifyLocal-Signature |
sha256= followed by the hex HMAC. See Verifying the signature. |
{ "event": "job.published", "occurred_at": "2026-04-13T09:02:11.000Z", "business": { "slug": "alpha-pressure-washing" }, "job": { "slug": "house-washing-marietta", "service_type": "house-washing", "service_city": "marietta", "sibling_slugs": ["house-washing-marietta-2"], "sibling_categories": [{ "service_type": "roof-cleaning", "service_city": "marietta" }] }, "delivery": { "id": "7a3f2c0e-5b1d-4e8a-9f6c-2d4b8e1a0c3f", "attempt": 1 }}| Field | Notes |
|---|---|
event |
One of the events above. |
occurred_at |
ISO 8601 UTC. |
business.slug |
The business the job belongs to. |
job.slug |
The job. Fetch it with Get a job, or drop it on job.unpublished. |
job.service_type, job.service_city |
The job’s service and city slugs, present only when the job has them. They name the index pages that list the job, which go stale with it. On job.unpublished they are the only way to learn which indexes it was on. |
job.sibling_slugs |
Other published pages whose content refers to this job. Present only when non-empty. Invalidate them along with the job. |
job.sibling_categories |
Service/city index pages that changed as a side effect of this event, beyond the job’s own. Present only when non-empty. |
delivery.id, delivery.attempt |
The delivery UUID (same as the header) and the attempt number. The body is generated once, so attempt reads 1 on retries too; dedupe on id. |
The full shape is WebhookPayload.
Responding
Section titled “Responding”Return any 2xx as fast as you can, before doing the work. A non-2xx response, a connection failure, or no response within 10 seconds counts as a failure and is retried. 4xx and 5xx are treated the same, so return 2xx even when your own processing fails and handle the failure on your side.
Verifying the signature
Section titled “Verifying the signature”Every delivery is signed with your endpoint’s secret.
signed_payload = X-SimplifyLocal-Timestamp + "." + raw request bodyexpected = HMAC-SHA256(secret, signed_payload), hex-encodedheader = "sha256=" + expectedVerify before you trust anything in the body:
- Read
X-SimplifyLocal-Timestamp. Reject the request if it is not a number or is more than 300 seconds from your clock. - Compute the HMAC over the raw body bytes exactly as received, prefixed with the timestamp and a period. Do not re-serialise the JSON first.
- Compare the hex you computed with the part of
X-SimplifyLocal-Signatureaftersha256=, using a constant-time comparison. - Reject on any mismatch with a
401.
Code in Node, PHP and Python is in Verify a webhook signature.
Retries
Section titled “Retries”A failed delivery is retried up to 7 times after the first attempt, at increasing intervals:
| Attempt | Delay after the previous failure |
|---|---|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
| 7 | 24 hours |
| 8 | 48 hours |
After the eighth failed attempt the delivery is dropped. Every retry carries the same X-SimplifyLocal-Delivery and the identical body (delivery.attempt is not updated between attempts), signed afresh with a current timestamp.
Dedupe
Section titled “Dedupe”Delivery is at-least-once. Make your handler idempotent on X-SimplifyLocal-Delivery: record the IDs you have processed and return 2xx without re-processing a repeat. Because the payload identifies rather than describes the change, replaying a delivery is harmless as long as your handler re-fetches rather than applies.
Circuit breaker
Section titled “Circuit breaker”Failures are counted per endpoint across deliveries, and any successful delivery resets the count to zero. After 50 consecutive failures the endpoint is disabled: no further deliveries are attempted, and pending retries for it are dropped. To re-enable it, fix the endpoint and email support.
Ordering
Section titled “Ordering”Deliveries are not guaranteed to arrive in order. Use occurred_at, or re-fetch the job, rather than assuming the latest delivery is the latest state.