Agent skill
Planning Center Webhooks Skill
Receive and verify Planning Center (church management) webhooks. Use when setting up Planning Center webhook subscription handlers, debugging X-PCO-Webhooks-Authenticity signature verification, or handling EventDelivery events such as people.v2.events.person.updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill planning-center-webhooks
When to Use This Skill
- How do I receive Planning Center webhooks?
- How do I verify the
X-PCO-Webhooks-Authenticitysignature? - Why does my Planning Center webhook signature verification fail?
- How do I handle
people.v2.events.person.updatedand other EventDelivery events? - How do I read the nested
attributes.payloadJSON string? - What status codes make Planning Center retry or deactivate a subscription?
How Planning Center Webhooks Work
- Webhook subscriptions are created at https://api.planningcenteronline.com/webhooks by any user who can log in (or by an OAuth application on a user's behalf). They only deliver data that user can see.
- Deliveries are
POSTrequests. The signature is in theX-PCO-Webhooks-Authenticityheader. - The body is a JSON:API document:
datais an array ofEventDeliveryobjects. Each hasattributes.name(the event, e.g.people.v2.events.person.updated),attributes.attempt, andattributes.payload, which is a JSON string that must be parsed a second time.
Signature Verification (core)
X-PCO-Webhooks-Authenticity is the bare hex HMAC-SHA256 of the raw request body, keyed with the subscription's authenticity_secret. Use the secret as a plain UTF-8 string. It looks like 64 hex characters, but do not hex-decode it.
const crypto = require('crypto');
function verifyPlanningCenter(rawBody, signature, secret) {
// Check the format first: Buffer.from(x, 'hex') silently truncates invalid hex
if (typeof signature !== 'string' || !/^[0-9a-f]{64}$/i.test(signature)) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
return crypto.timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}
import hashlib, hmac
def verify_planning_center(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
# Bytes on both sides: a non-ASCII header gives False (401), not a TypeError (500)
return hmac.compare_digest(expected.encode(), signature.lower().encode())
In Express, read the raw body with express.raw({ type: '*/*' }) so any content type verifies. There is no timestamp in the signature; use EventDelivery.id for idempotency (see below).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/. The tests reproduce the worked example from Planning Center's docs.
Event Names
The public docs name one event, taken from their worked example:
| Event | Triggered When |
|---|---|
people.v2.events.person.updated | A Person record changes in Planning Center People |
Event names follow <app>.<api version>.events.<resource>.<action>. The full catalog of subscribable events is shown in the Webhooks UI after login (and via the authenticated available_events API); it is not published on the public docs, so check it there rather than guessing names. See references/overview.md.
Handling the Payload
for (const delivery of body.data) { // EventDelivery objects
const { name, attempt, payload } = delivery.attributes;
const event = JSON.parse(payload); // nested JSON string
// event.data is the changed resource, e.g. { type: 'Person', id: '125587227', attributes: {...} }
}
Responses and Retries
- Respond
200to acknowledge. Any other status is retried up to 16 times (30 s, 1 min, 2 min, ... up to once per day), then the subscription is deactivated. - Responding
410deactivates the subscription immediately. Don't return it by accident. 429or503with aRetry-Afterheader is honored (capped at 1 hour).- Use
delivery.idto deduplicate retries and manual redeliveries.
Environment Variables
# From https://api.planningcenteronline.com/webhooks -> your subscription
PLANNING_CENTER_AUTHENTICITY_SECRET=your_authenticity_secret_here
Local Development
npx hookdeck-cli listen 3000 planning-center --path /webhooks/planning-center
No account required. Use the Hookdeck URL as the subscription URL.
Reference Materials
- references/overview.md - Webhook concepts, payload structure, event naming
- references/setup.md - Creating a subscription and finding the authenticity secret
- references/verification.md - Signature details, worked example, gotchas