Outbound webhooks
Receive events from Groupilot at your URL, the envelope, the data of each event type, the HMAC signature, retries and the endpoint API.
Groupilot can call your server when something happens in your account: a member joins, a message is sent or fails, a community is created or fills up, the number disconnects, an event is paused, a broadcast ends. Reference: Webhooks.
Two kinds of webhooks exist, with the same envelope, signature and retries:
| Kind | Scope | Configured in | Events |
|---|---|---|---|
| Account webhooks | The whole account | Settings, Integrations, Webhooks, or POST /v1/webhooks | Any of the 8 types below |
| Member webhook | One event | Event settings, or PUT /v1/events/{id}/member-webhook (see Member webhook by API) | member.joined only, with the ids of the guide page |
Event types and their data
| Type | When | data |
|---|---|---|
member.joined | Someone joined a WhatsApp community of one of your events | event_id, occurrence_id, community_id, whatsapp_id, community_name, member: { phone, name? } |
job.sent | A scheduled message was sent, to all or part of its communities | job_id, event_id, occurrence_id, source (sequence or ad_hoc), sent_at, targets_to_verify |
job.failed | A scheduled message failed for good, no more automatic retry | job_id, event_id, occurrence_id, source, category, reason |
community.created | A WhatsApp community was created for an occurrence | community_id, occurrence_id, event_id, whatsapp_id, name, number |
community.full | A community reached its switch threshold, the next one is open | same as community.created plus member_count |
channel.disconnected | The WhatsApp number disconnected | channel_id, source (webhook or health_monitor), detected_at |
event.paused | An event was paused | event_id, name |
broadcast.completed | A broadcast finished | broadcast_id, status (completed, failed, requires_review), success_count, failure_count, uncertain_count, community_count, sent_at |
webhook.test | You sent a test | test: true |
Ids are the public API ids: the same ones you get from GET /v1/events, /occurrences, /communities and /jobs.
The envelope
{
"id": "6f1c2d0e-…",
"type": "member.joined",
"version": "1",
"occurred_at": "2026-11-06T09:12:44+01:00",
"project_id": "…",
"data": {
"event_id": "…",
"occurrence_id": "…",
"community_id": "…",
"whatsapp_id": "1203630…@g.us",
"community_name": "Webinar Marie - 09/11",
"member": { "phone": "+33612345678", "name": "Julie" }
}
}Headers: Content-Type: application/json, X-Groupilot-Event-Id (same as id), X-Groupilot-Event-Type, X-Groupilot-Signature.
Delivery is at least once: a retry reuses the same id. Deduplicate on it.
Signature
X-Groupilot-Signature: t=1762420364,v1=5f3a…
v1 is the hex HMAC-SHA256 of t + "." + rawBody with the signing secret of the endpoint (whsec_…, shown once at creation or rotation). Reject messages whose t is older than 5 minutes.
import crypto from "node:crypto";
export function verify(rawBody, sig, secret) {
const [t, v1] = sig.split(",").map((p) => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return (
expected.length === v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) &&
Math.abs(Date.now() / 1000 - Number(t)) < 300
);
}Verify on the raw body, before any JSON parsing or re-serialisation.
Retries
- Answer
2xxwithin 5 seconds. Process the message afterwards. - Anything else (timeout, network error, non-
2xx) is retried 6 more times over about 24 hours: after 1 min, 5 min, 30 min, 2 h, 6 h and 15 h. - A delivery has a
status(pending,processing,delivered,failed,skipped),attempt_count,max_attempts,last_http_status,last_error_codeandnext_attempt_at. 30 days of history. - While an endpoint is paused (
enabled: false), nothing is queued for it. - After 50 consecutive deliveries failed for good, the endpoint is paused automatically:
enabledbecomesfalsewithdisabled_reason: "failures"("user"when you paused it yourself), andconsecutive_failurescounts the failures since the last success. Fix your side, thenPATCHwithenabled: true: deliveries resume and the counter resets.
Manage endpoints by API
read scope for the GETs, write for the rest. 5 endpoints per account. HTTPS, public host, no private address and no credentials in the URL.
POST /v1/webhooks
{ "url": "https://hooks.example.com/groupilot", "events": ["member.joined", "job.failed", "community.full"], "description": "CRM sync" }The 201 response is the endpoint plus signing_secret, shown once.
| Call | Does |
|---|---|
GET /v1/webhooks, GET /v1/webhooks/{id} | List and read |
PATCH /v1/webhooks/{id} | Change url, events, description; enabled: false pauses, true resumes, including after an automatic pause |
DELETE /v1/webhooks/{id} | Removes it with its delivery history |
POST /v1/webhooks/{id}/rotate-secret | New signing_secret for the next deliveries. Update your verifier first, then rotate |
POST /v1/webhooks/{id}/test | Posts a webhook.test now and returns the first attempt: status (delivered, failed, pending), http_status, error_code |
GET /v1/webhooks/{id}/deliveries | Newest first, limit and before to page |
In the app
Settings, Integrations, Webhooks card: add an address, tick the events, copy the secret shown once, then use Test, Pause, New secret, Delete and Deliveries (last 10).