Subscriber created
Someone became a subscriber of the newsletter. Five paths land here: subscribing from inside the Commune app, finishing signup, accepting an invitation, being subscribed to Commune's own newsletter, and Add a subscriber (POST /newsletters/{newsletter}/subscribers).
An import does not land here, and that is deliberate. Neither a CSV upload nor an ESP import nor the daily provider sync fires this topic: an import writes many rows at once and must not fan out one message per row, so it publishes a single import.completed instead. Nothing that keys off this topic therefore reaches an imported list, which is what keeps a welcome sequence off a list the creator migrated in.
data.actor is what tells the API path apart from the four the product reaches. It names the credential on a POST, and is null when a person did something in Commune itself.
data.acquisition_source records which import first brought an address and is fixed when the row is created, so it is null for anybody first recorded through Commune. A row that was first imported reaches this topic only by coming back (see below), and carries that provider's slug or csv when it does.
Reactivation counts. Unsubscribing does not delete a subscriber, so a returning reader comes back to subscribed rather than being recorded again. That still fires this topic, with data.resubscribed: true. Setting status back to subscribed through Update a subscriber (PATCH /subscribers/{subscriber}) fires it the same way, and so does a POST naming an address that is already on the list but inactive. Branch on resubscribed before you welcome anybody.
Delivered as a single HTTPS POST to the consumer's registered endpoint, with the message as the JSON request body. Respond 2xx to acknowledge. Anything else, or a timeout, is retried with backoff, so acknowledge fast and do the work afterwards.
Headers
API version the payload conforms to. Same value as api_version in the envelope.
Same value as id in the envelope. The dedupe key.
Same value as type in the envelope. The topic name.
HMAC-SHA256 over Commune-Timestamp as Unix seconds, a literal ., then the raw request body, keyed by the endpoint's signing secret. Lowercase hex, carried as v0= followed by one or more comma separated digests (more than one while a secret is being rotated). The header's timestamp is an RFC 3339 instant, so convert it to Unix seconds before signing. Verify against the raw bytes before parsing the JSON. The webhooks guide has the full procedure.
When this delivery attempt was made, as an RFC 3339 instant in UTC. It differs on every retry, and it IS covered by the signature, in Unix seconds, so a stale or altered one can be rejected. Enforce a tolerance window of a few minutes and dedupe on the event id as well, since a retry inside that window is legitimate.
1 on the first attempt, incremented on each retry. Not sent today: do not rely on it, and be idempotent regardless. The same number is readable after the fact as attempt on a DeliveryAttempt, so a consumer that needs to tell a retry from a first delivery can ask instead of being told.
Payload
Example payload
{
"id": "018f2a8b-6c4b-7d2e-9f11-6a1c3d5e7b90",
"type": "article.published",
"api_version": "2026-08-26"
}Response
The consumer accepted the delivery. Any 2xx acknowledges the message and it is not sent again.
Changes
Webhooks have no change history: a webhook is keyed by its event name rather than a path and method, so no recorded change can name it.