Skip to content

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.

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.

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.
POST {your URL}
Content-Type: application/json
User-Agent: SimplifyLocal-Webhook/1.0
X-SimplifyLocal-Event: job.published
X-SimplifyLocal-Delivery: 7a3f2c0e-5b1d-4e8a-9f6c-2d4b8e1a0c3f
X-SimplifyLocal-Timestamp: 1715000000
X-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.

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.

Every delivery is signed with your endpoint’s secret.

signed_payload = X-SimplifyLocal-Timestamp + "." + raw request body
expected = HMAC-SHA256(secret, signed_payload), hex-encoded
header = "sha256=" + expected

Verify before you trust anything in the body:

  1. Read X-SimplifyLocal-Timestamp. Reject the request if it is not a number or is more than 300 seconds from your clock.
  2. 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.
  3. Compare the hex you computed with the part of X-SimplifyLocal-Signature after sha256=, using a constant-time comparison.
  4. Reject on any mismatch with a 401.

Code in Node, PHP and Python is in Verify a webhook signature.

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.

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.

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.

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.