Webhooks
What the service registers on a site, how it verifies deliveries, and why reconciliation exists.
On connect the service registers, through the Admin API, one webhook per event it listens to, each with its own target URL https://<app host>/hooks/ghost/<site>/<generation>/<event> and a random secret of its own: post.published, post.published.edited, post.unpublished, post.scheduled, post.unscheduled, post.rescheduled, post.deleted, post.edited, member.added, member.edited, member.deleted.
Verification
Every delivery is checked on the raw body: X-Ghost-Signature: sha256=<hmac>, t=<ms> is HMAC-SHA256 of the body followed by the timestamp, with the webhook's secret; the timestamp must be within five minutes. A delivery is recorded once per (event, resource id, resource updated_at); a duplicate is acknowledged and not processed again. The service answers 2xx quickly and does the work afterwards: it never trusts the payload, and re-reads the post or member from the Admin API before changing anything. It answers 410, which makes Ghost delete the webhook, only on purpose: for a site that was disconnected or purged, and for a webhook generation a reconnect has replaced.
What Ghost does not do
Ghost delivers each webhook once, with a two-second timeout, and does not retry a failed POST. Renewals of a subscription fire no member webhook at all. The service therefore treats webhooks as hints: a daily check reads every post with a submission back from the site, and a scheduled poll reads each contributor's subscription, so a lost delivery costs at most a day.
Reconnect
A reconnect registers a new generation of webhooks; a delivery to an old generation is answered 410 so Ghost drops that hook itself.