API Overview
Planning Center Online (PCO) exposes a REST API conforming to the JSON:API 1.0 specification. Each PCO product has its own versioned API endpoint.
Base URLs
| Product | Base Path | API Version |
|---|---|---|
| People | https://api.planningcenteronline.com/people/v2/ | 2022-07-14 |
| Check-Ins | https://api.planningcenteronline.com/check-ins/v2/ | 2019-07-17 |
| Groups | https://api.planningcenteronline.com/groups/v2/ | 2018-08-01 |
| Calendar | https://api.planningcenteronline.com/calendar/v2/ | 2021-07-20 |
| Services | https://api.planningcenteronline.com/services/v2/ | — |
| Giving | https://api.planningcenteronline.com/giving/v2/ | — |
JSON:API 1.0 Request/Response Format
All responses follow JSON:API structure with data, included (sideloaded relationships), links (pagination), and meta sections.
// GET response (single resource)
{
"data": {
"type": "Person",
"id": "12345",
"attributes": {
"first_name": "John",
"last_name": "Smith",
"remote_id": 42 // your system's ID stored on PCO
},
"relationships": { ... }
},
"included": [ ... ], // sideloaded via ?include=emails,phone_numbers
"links": { "next": "..." } // cursor-based pagination
}
// POST/PATCH request body
{
"data": {
"type": "Person",
"attributes": {
"first_name": "John",
"last_name": "Smith"
}
}
}
Key Query Features
- Sideloading:
?include=emails,phone_numbers— fetch related resources in a single request. - Filtering:
?where[first_name]=John— field-level filtering. - Search:
?where[search_name_or_email_or_phone_number]=...— combined search. - Ordering:
?order=last_nameor?order=-created_at(descending). - Pagination:
?per_page=100(max 100). Followlinks.nextURL for next page.
Authentication
PCO offers two authentication methods. Both produce the same API access.
Option 1: Personal Access Token (PAT)
Best for single-org server-to-server integration. No user interaction required after initial setup.
- Created at
api.planningcenteronline.com/oauth/applications - Uses HTTP Basic Auth:
application_id:secret - Full read/write access to that org's data
- Never expires (until revoked)
# Example: List all people with PAT
curl -u "app_id:secret" \
https://api.planningcenteronline.com/people/v2/people?per_page=100
Option 2: OAuth 2.0
Best for multi-org apps where each church authorizes access independently.
- Authorization:
api.planningcenteronline.com/oauth/authorize - Token exchange:
api.planningcenteronline.com/oauth/token - Access tokens expire after 2 hours
- Refresh tokens are long-lived
- Scopes:
people,check_ins,groups,calendar,services,giving
Write Capabilities Matrix
This is the most important constraint for integration planning. PCO's API is heavily read-biased — most resources are read-only.
| Resource | Module | Create | Update | Delete | Notes |
|---|---|---|---|---|---|
| Person | People | Yes | Yes | Yes | remote_id field for storing your system's ID |
| People | Yes | Yes | Yes | Scoped to person: /people/{id}/emails |
|
| Phone Number | People | Yes | Yes | Yes | Scoped to person. PCO auto-normalizes to E.164 |
| Address | People | Yes | Yes | Yes | On Person (not Household) |
| Household | People | Yes | Yes | Yes | Maps to your families table |
| Household Membership | People | Yes | Yes | Yes | No relationship role — just "is a member" |
| Group Membership | Groups | Yes | Yes | No | Role: "leader" or "member" only |
| Group | Groups | Read-Only | Read-Only | No | Groups created in PCO admin only |
| Group Type | Groups | Read-Only | Read-Only | No | — |
| Check-In | Check-Ins | Read-Only | Read-Only | No | Created via PCO Check-Ins app or Church Center only |
| Check-In Event | Check-Ins | Read-Only | Read-Only | No | Configured in PCO admin only |
| Station / Location / Label | Check-Ins | Read-Only | Read-Only | No | All check-in infrastructure is read-only |
| Calendar Event | Calendar | Read-Only | Read-Only | No | Events created in PCO Calendar admin only |
| Event Instance | Calendar | Read-Only | Read-Only | No | Individual occurrences of recurring events |
People API Read/Write
The People API is the most complete for integration. Full CRUD on people, emails, phones, addresses, and households.
Person Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
GET | /people/v2/people | List all people (paginated, max 100/page) |
GET | /people/v2/people?where[search_name_or_email_or_phone_number]=... | Search by name, email, or phone |
GET | /people/v2/people?include=emails,phone_numbers | Sideload contacts in one request |
GET | /people/v2/people/{id} | Get single person |
POST | /people/v2/people | Create person |
PATCH | /people/v2/people/{id} | Update person |
DELETE | /people/v2/people/{id} | Delete person |
Writable Person Attributes
first_name, last_name, given_name, nickname,
middle_name, birthdate (YYYY-MM-DD), gender,
child (boolean), grade, graduation_year,
medical_notes, status, membership,
avatar, remote_id (integer — store your system's people.id here)
remote_id field (writable integer on Person) is designed for exactly this
use case — storing an external system's ID on the PCO record. Set this to your
people.id to enable reliable bidirectional lookup without maintaining a separate
mapping table.
Contact Endpoints (scoped to person)
| Resource | Create | List | Writable Attributes |
|---|---|---|---|
POST /people/v2/people/{id}/emails |
GET /people/v2/people/{id}/emails |
address, location (label), is_primary |
|
| Phone | POST /people/v2/people/{id}/phone_numbers |
GET /people/v2/people/{id}/phone_numbers |
number, location (label), is_primary |
| Address | POST /people/v2/people/{id}/addresses |
GET /people/v2/people/{id}/addresses |
street, city, state, zip, location, is_primary |
Household Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
GET | /people/v2/households | List all households |
POST | /people/v2/households | Create household |
PATCH | /people/v2/households/{id} | Update household |
POST | /people/v2/households/{id}/household_memberships | Add person to household |
DELETE | /people/v2/households/{id}/household_memberships/{mid} | Remove from household |
Household writable attributes: name, primary_contact_id, avatar.
Groups API Partial Write
Groups and group types are read-only. Only memberships can be created and updated via the API.
Read-Only Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
GET | /groups/v2/groups | List all groups |
GET | /groups/v2/groups/{id} | Get single group (name, description, enrollment, schedule) |
GET | /groups/v2/group_types | List group types (category) |
GET | /groups/v2/events | Group-specific events |
Group attributes (read-only): name, description, enrollment_open,
enrollment_strategy, memberships_count, schedule (freetext),
header_image, location_type_preference.
Writable: Memberships
| Method | Endpoint | Purpose |
|---|---|---|
GET | /groups/v2/groups/{id}/memberships | List group members |
POST | /groups/v2/groups/{id}/memberships | Add member to group |
PATCH | /groups/v2/groups/{id}/memberships/{mid} | Update membership (role, joined_at) |
Membership writable: person_id (write-only), role ("leader"
or "member" only), joined_at.
Check-Ins API Read-Only
The entire Check-Ins API is read-only. Check-in records are created through the PCO Check-Ins iPad app or Church Center. This is the biggest constraint for integration.
Data Model Hierarchy
Key Read Endpoints
| Endpoint | Purpose | Key Attributes |
|---|---|---|
GET /check-ins/v2/check_ins |
List all check-in records | first_name, last_name, security_code, medical_notes, kind, checked_out_at |
GET /check-ins/v2/events |
List check-in events | name, frequency |
GET /check-ins/v2/event_times |
List event time slots | starts_at, shows_at, hides_at, total_count, guest_count, volunteer_count |
GET /check-ins/v2/stations |
List check-in stations | name, online, mode, check_in_count |
GET /check-ins/v2/labels |
Label templates | name, xml (template), prints_for, roll |
GET /check-ins/v2/headcounts |
Aggregate counts | total (by AttendanceType) |
Useful Check-In Queries
# Recent check-ins with person and event data sideloaded
GET /check-ins/v2/check_ins
?include=event,person,locations
&order=-checked_out_at
&per_page=100
# Headcounts by event time
GET /check-ins/v2/event_times
?include=headcounts
&filter=future
Location Model (Hierarchical)
PCO Locations are hierarchical: a Location with kind="Folder" contains child locations.
Each location has age/grade filtering: age_min_in_months, age_max_in_months,
grade_min, grade_max, gender, child_or_adult,
max_occupancy, min_volunteers, attendees_per_volunteer.
This is analogous to Church Management Platform's hierarchical church_group system with
parent_group_id, but PCO uses it for room-based assignment rather than
organizational-group-based event resolution.
Calendar API Read-Only
Calendar events and instances are read-only. Events are created and managed in PCO Calendar admin.
Key Endpoints
| Endpoint | Purpose | Key Attributes |
|---|---|---|
GET /calendar/v2/events |
List calendar events (templates) | name, description, approval_status, visible_in_church_center |
GET /calendar/v2/event_instances |
Specific occurrences | starts_at, ends_at, recurrence, recurrence_description, location |
GET /calendar/v2/event_instances?filter=future&order=starts_at |
Upcoming events | — |
Resource Booking
PCO Calendar includes room/resource booking with ResourceBooking, Resource,
ResourceFolder, and ResourceApprovalGroup resources. All read-only via API
but useful for pulling room assignment data into Church Management Platform's locations.
Entity Mapping
Side-by-side comparison of Church Management Platform entities and their PCO equivalents.
| Church Management Platform | PCO Equivalent | Sync Direction | Notes |
|---|---|---|---|
people |
Person (People API) |
Push & Pull | Use remote_id on PCO to store your people.id |
person_emails |
Email (People API) |
Push & Pull | label → location |
person_phones |
PhoneNumber (People API) |
Push & Pull | Both store E.164. PCO auto-normalizes |
families |
Household (People API) |
Push & Pull | family_name → name |
family_members |
HouseholdMembership |
Push & Pull | PCO has NO relationship role (Parent/Child/Spouse) |
church_group |
Group (Groups API) |
Pull Only | Read-only. Your hierarchy is richer (3-level vs flat) |
group_type |
GroupType (Groups API) |
Pull Only | Read-only |
group_membership |
Membership (Groups API) |
Push & Pull | PCO role: "leader"/"member" only (no Coach/Other) |
checkins |
CheckIn (Check-Ins API) |
Pull Only | Cannot push check-ins to PCO |
events |
EventInstance (Calendar API) |
Pull Only | Read-only. Your group_id link has no PCO equivalent |
locations |
Location (Check-Ins API) |
Pull Only | PCO locations are hierarchical with age/grade filters |
small_group |
No direct equivalent | N/A | Your small group metadata (meeting day, area, childcare) has no PCO home |
forms |
Form (People API) |
Pull Only | PCO forms are different (workflow forms vs registration) |
| Family address | Address on Person |
Push & Pull | PCO puts addresses on Person, not Household |
people.allergies |
Person.medical_notes |
Push & Pull | PCO uses freetext; yours uses text[] array |
Model Differences & Gaps
Family / Household Structure
- PCO HouseholdMembership has no relationship role (no Parent/Child/Spouse). PCO determines
child status from the Person's
childboolean flag. - PCO addresses are on Person (not Household). Each person can have multiple addresses.
Your system stores address on
families. - Your
authorized_pickup_idsJSONB array has no PCO equivalent. - Your
relationshipstable (Parent, Child, Guardian, Spouse, Grandparent) has no PCO match.
Groups
- Your 3-level hierarchy (Department → Area → Group via
church_group.level) is richer than PCO's flat GroupType → Group. - PCO membership role is only
"leader"or"member"— no equivalent to yoursmall_group_leader_type(Leader/Coach/Other). - Your small group metadata (meeting day, frequency, time, area, childcare, demographics) has no PCO
equivalent. PCO stores
scheduleas a single freetext string. - PCO
enrollment_strategyandenrollment_openmap loosely to yourjoin_rule_id.
Check-Ins
- PCO uses a Station model (physical iPads) vs. your software kiosk.
- PCO resolves children to rooms via Location age/grade filters. Your system uses
hierarchical group-based event resolution (walking
parent_group_idchain). - Your smart check-in window (5h/30min logic) has no PCO equivalent — PCO uses manual
shows_at/hides_atconfiguration per station. - PCO Headcounts are a separate concept from individual check-ins — aggregate
counts by
AttendanceType(e.g. "Regulars", "Guests", "Volunteers").
Events / Calendar
- PCO Calendar Event → EventInstance maps to your
recurrence_group_idsystem. - Your
events.group_id(linking events to church groups for kiosk auto-resolution) has no PCO equivalent. PCO has separate Check-Ins Events and Calendar Events (can be integrated but distinct).
Webhooks
PCO supports webhooks for real-time change notifications, reducing the need for polling.
Available Events
Confirmed for the People module:
| Event | Fires When |
|---|---|
people.v2.events.person.created | New person added in PCO |
people.v2.events.person.updated | Person record modified |
people.v2.events.person.destroyed | Person deleted from PCO |
Check-Ins webhooks have been requested but not confirmed as available (GitHub issue #1332 closed without resolution).
Webhook Payload
{
"data": [{
"id": "webhook-delivery-id",
"attributes": {
"name": "people.v2.events.person.updated",
"payload": {
"data": {
"type": "Person",
"id": "12345",
"attributes": { ... }
}
}
}
}]
}
Security
- Verify via
X-PCO-Webhooks-Authenticityheader using HMAC-SHA256 with your webhook secret. - Use timing-safe comparison to prevent timing attacks.
Retry Policy
- Up to 16 retries with exponential backoff over 5+ days.
- Email alerts after 1 hour of failure and after final attempt.
- Subscription auto-deactivated after all retries exhausted.
Rate Limits & Pagination
Rate Limit: 100 Requests / Minute
PCO enforces 100 requests per minute per organization. No documented burst allowance. No batch/bulk endpoints exist — each person, email, phone, address, household requires its own API call.
Impact on Bulk Operations
| Operation | API Calls | Time at Rate Limit |
|---|---|---|
| List 5,000 people (with sideloaded contacts) | 50 GET requests (100/page) | 30 seconds |
| Push 5,000 people (create/update) | 5,000 requests | ~50 minutes |
| Push 5,000 people + emails + phones | ~15,000 requests | ~2.5 hours |
| Full initial sync (people + families + memberships) | ~25,000+ requests | ~4+ hours |
Pagination
- Max
per_page=100. - Cursor-based via
links.nextURL in response. No offset parameter. - Must follow
links.nextURLs sequentially — cannot jump to page N.
Mitigation Strategies
- Sideload aggressively —
?include=emails,phone_numbers,addressesto get everything in one request per page. - Incremental sync — use
?where[updated_at][gte]=2026-02-20T00:00:00Zto pull only records changed since last sync. - Webhooks for real-time — subscribe to person.created/updated events instead of polling.
- Queue writes — use a background job queue (with rate limiter) to spread write operations across time windows.
- Off-peak scheduling — run bulk syncs during low-activity hours (e.g., overnight).
Integration Strategies
Three viable approaches, depending on the church's needs:
PCO as Source of Truth
Pull people, groups, and events from PCO into Church Management Platform. Your system is the better check-in frontend that reads PCO data. Check-ins happen locally only.
- Church manages people/groups in PCO
- Periodic sync pulls changes into your DB
- Webhooks for real-time person updates
- Check-in data stays in your system
Church Management Platform as Source of Truth
Push people and families to PCO so the church sees data in both systems. Groups and events stay local. PCO is a read-only mirror.
- New people created in your system → pushed to PCO
- Updates flow your system → PCO
- Groups/events managed locally
- Church uses PCO for features you don't have (Giving, Services)
Bidirectional Where Possible
Pull groups/events from PCO (read-only). Push people/families to PCO (writable). Sync memberships both ways. Check-ins stay local.
- Most complex to build
- Conflict resolution needed (last-write-wins?)
- Memberships: push new adds, pull PCO-originated joins
- Use
remote_id+updated_atfor change detection
Sync Architecture
Regardless of which strategy is chosen, the sync system would follow this general architecture.
Sync Service Components
Sync Mapping Table
CREATE TABLE pco_sync_map (
id SERIAL PRIMARY KEY,
local_type TEXT NOT NULL, -- 'person', 'family', 'group', 'membership'
local_id INTEGER NOT NULL,
pco_id TEXT NOT NULL, -- PCO uses string IDs
pco_updated_at TIMESTAMPTZ,
last_synced_at TIMESTAMPTZ,
sync_status TEXT DEFAULT 'synced', -- 'synced', 'pending_push', 'pending_pull', 'conflict'
UNIQUE(local_type, local_id),
UNIQUE(local_type, pco_id)
);
Sync Flow: Pull People from PCO
triggers sync
?include=emails,phones
?where[updated_at][gte]=...
for existing match
local people table
last_synced_at
Sync Flow: Push People to PCO
in Church Management Platform
(rate-limited)
+ POST emails, phones
+ POST household
in pco_sync_map
on PCO Person
Conflict Resolution
For bidirectional sync, conflicts occur when both systems modify the same record between syncs.
- Last-write-wins — compare
updated_attimestamps. Simplest but can lose data. - Source-of-truth per field — e.g., PCO owns email, your system owns allergies. More complex but no data loss.
- Flag for review — mark conflicts in
sync_status='conflict'for admin resolution. Safest but requires manual intervention.
Webhook Handler Endpoint
// New endpoint in a PcoWebhookController
POST /api/pco/webhook
// 1. Verify X-PCO-Webhooks-Authenticity header (HMAC-SHA256)
// 2. Parse JSON:API payload
// 3. Route by event name:
// person.created -> upsert into people table
// person.updated -> update people table
// person.destroyed -> soft-delete (set is_active=false)
// 4. Update pco_sync_map
// 5. Return 200 OK