There are two versions of this question, and they arrive about six months apart.
The first is how do I export this? Someone needs a list of households for a mailing, or last quarter’s giving for the finance team, and the answer is a CSV button somewhere in Planning Center.
The second is how do I keep a copy of this? Someone has been exporting the same six files every Monday for half a year, the spreadsheet has grown a tab per ministry, and the obvious next thought is: there’s an API, I’ll just automate it.
That second question is a bigger commitment than it looks. This piece covers both honestly: what each product gives you as a file, what the API actually does, and where the line sits between a script worth writing and an engineering project you’ve accidentally signed up for.
What each product exports
Planning Center has no central export screen. Like reporting generally, it’s per-product, and each product has its own idea of what an export is.
People is the most capable. Build a List with your rules, then export the matching people to CSV. You choose whether the file carries every field on those profiles or only the columns displayed on the list, so check which mode produced the file you’re holding before you build on it. There’s also a separate whole-database export covering every profile, delivered as an emailed download link. This is the export most churches live on.
Giving exports donations for a date range, filterable by fund and payment source. Separately it produces donor statements, which are documents for people, not data for you. Don’t confuse the two. And check what identifier your export carries before you build on it: Giving’s “donor numbers” are opt-in envelope numbers, not the person ID. The one column position Planning Center documents is that donor numbers sit second in the donor-list export; for everything else, open your file and look.
Check-Ins exports come in two shapes that are easy to confuse. The event chart’s CSV and Headcount Reports are bucketed totals over a timeframe, and a Headcount Report caps at 53 past sessions, roughly a year of weekly services. Custom reports are the person-level ones, giving you rows per person across selected sessions, and Planning Center’s own advice for multi-year history is to run one report per year and combine the CSV files.
Groups reports and exports cover membership and event attendance for individual or multiple groups. Services has several different exports: its generic People CSV is profile-oriented, while the service-type People report exports plan-level scheduling status and attendance over a chosen timeframe. Registrations exports attendee rosters per signup. Name the exact surface in your instructions; “the Services CSV” is not precise enough.
Every one of those named exports exists today, but Planning Center moves the buttons: a download icon in one product, an Actions dropdown in another, a gear menu in a third. Check the current help article for the product you’re in rather than trusting a screenshot. Exporting is permission-gated separately by product. Can export CSVs and reports is a People permission; Services, Groups, Giving and the other products apply their own roles and access rules.
Two things are true of every one of those files.
They’re flat. Each export is one product’s view of the world. Nothing in the Giving CSV tells you whether that household attended in March, and nothing in the Check-Ins CSV tells you whether they serve. Joining them is your job. Use a stable identifier that you have verified exists in both real files; do not assume every CSV includes the People ID, and do not substitute donor number, email or name without an explicit mapping policy. Name matching quietly breaks on “Mike” versus “Michael,” marriages and households that give under two names.
And they’re snapshots. The file reflects the selected data at download time and becomes stale as records change. That’s the whole problem, and it’s why the API question comes up.
The API, accurately
Planning Center’s API is a clean, conventional JSON:API implementation. If you’ve worked with JSON:API before, you already know most of it.
The shape is https://api.planningcenteronline.com/{product}/v2/{resource}, so People lives under /people/v2/people, donations under /giving/v2/donations, check-ins under /check-ins/v2/check_ins. Each product is effectively its own API that happens to share a hostname, a token and a response envelope.
Authentication is either a Personal Access Token over HTTP Basic, with the client ID as the username and the secret as the password, which is fine for scripts against your own organization, or OAuth, which is required for a distributed integration that other churches authorize. Scopes are granted per product, so a token that can read People cannot necessarily read Giving. Planning Center also requires an identifying User-Agent on API requests; omit it and you can get a 403 before you’ve done anything else wrong.
curl -u "$PCO_CLIENT_ID:$PCO_SECRET" \
-H "User-Agent: Example Church reporting script (data@example.org)" \
"https://api.planningcenteronline.com/people/v2/people?per_page=100"
Responses come back as data (an array of typed resources with id, attributes, relationships), plus included when you ask for it, plus a meta block and links.next for paging.
include= is the single most useful parameter
The naive way to build a report is a fan-out: fetch 2,000 people, then make a separate email and household request for each person. Ignoring the requests needed to page through People itself, that is about 4,000 child requests.
include= collapses it. Ask for the related resources alongside the primary ones and they come back side-loaded in included, matched by relationship IDs:
curl -u "$PCO_CLIENT_ID:$PCO_SECRET" \
-H "User-Agent: Example Church reporting script (data@example.org)" \
"https://api.planningcenteronline.com/people/v2/people?include=emails,households,primary_campus&per_page=100"
At per_page=100, the primary People collection is about twenty paged requests, with related resources side-loaded where that relationship supports include. That is a large reduction, but it is not automatically identical to every possible child-resource fan-out: verify cardinality, pagination and which relationships the endpoint permits.
The counterweight: includes are not free. A wide include on a resource with a large child collection can turn a small response into a very large one. We have endpoints where a single tempting-looking include inflates the payload by two orders of magnitude, to the point where it’s cheaper to make the extra requests. Measure the response size before you commit to an include, not after.
Pagination
Offset-based: per_page (25 by default, 100 the documented maximum) and offset, with meta.total_count telling you how far you have to go and links.next handing you the next URL.
The gotcha is that offset pagination is not stable against a moving dataset. If records are created or updated while you’re on page 40 of 180, rows shift between pages and you will silently miss some. For a nightly pull of a large church this is not hypothetical. The mitigations are ordering by something stable, keeping windows short, and re-reconciling periodically, all of which is code you now own.
Rate limits
Planning Center tells you exactly where you stand on every response:
X-PCO-API-Request-Rate-Count: 47
X-PCO-API-Request-Rate-Limit: 100
X-PCO-API-Request-Rate-Period: 20
The published default is 100 requests per 20 seconds, applied per authenticated user rather than per application or organization. Deep paging is throttled harder: past an offset of 30,000 the limit drops to 75 per 20 seconds. Exceed either and you get 429 with a Retry-After.
Planning Center is explicit that you shouldn’t hard-code any of this: individual endpoints can enforce their own higher or lower limits, and the numbers can be adjusted at any time without notice. The headers are the contract; 100 is just today’s default.
A naive script handles this with a retry. A sync you can leave running handles it by pacing: reading those headers and shaping the request rate to stay under the limit deliberately, rather than sprinting into a wall and backing off. We run a pacer that targets a fixed fraction of the published limit and falls back to a conservative default when the headers are absent, plus bounded retries on top. That distinction, pacing versus retrying, is most of the difference between a sync that finishes and one that thrashes.
The part that decides whether this is a weekend or a quarter
Everything above is a solvable afternoon. Here’s what isn’t.
Incremental sync only works on some endpoints. To pull only what changed, you need updated_at to be both orderable and filterable on that endpoint. Plenty of Planning Center endpoints don’t offer both. In August 2026 we ran a capability probe across the 109 top-level list endpoints in the OpenAPI descriptions Planning Center publishes per product and version, linked as “OpenAPI Description” from each product’s page in the API reference. No token is needed, so anyone can re-run it. We got this:
| Product | Endpoints probed | Incremental-capable | Full-sync only |
|---|---|---|---|
| People | 36 | 15 | 21 |
| Calendar | 18 | 8 | 10 |
| Giving | 12 | 4 | 8 |
| Check-Ins | 11 | 2 | 9 |
| Services | 13 | 0 | 13 |
| Groups | 9 | 0 | 9 |
| Registrations | 6 | 0 | 6 |
| Publishing | 4 | 0 | 4 |
| Total | 109 | 29 | 80 |
27% meet that specific updated_at filter-and-order criterion. The other 80 are not incrementally safe by this test. They may require full reconciliation, a nested route, a created_at strategy, webhooks or another resource-specific plan. Across Services, Groups, Registrations and Publishing, no top-level list endpoint meets the criterion, which is the number that reshapes the project.
One qualification, because it’s the difference between awkward and impossible. Groups, Registrations and Publishing have no incremental-capable endpoint anywhere, top-level or nested. Services has a handful, but none of them are top-level: they hang off nested paths, so keeping Services current means walking the tree beneath every service type rather than making one filtered call. That’s a different and slower shape of job, and it’s the kind of thing that turns a scheduled script into a system.
It’s also a moving target, and the drift is measurable. Endpoint capabilities change between API versions, each product versions independently, and endpoints appear inside a version too: Groups gained two top-level list endpoints between two of our probe runs in August 2026, with its version string unchanged. Services still defaults to a 2018 version while People is on one from 2026. A capability audit is not a one-time task; it’s something you re-run, and each re-run moves cells.
The table above is from our August 2026 run. We re-run the audit at least every 90 days, with a CI job that files an issue at us when we’re late, because it’s the same audit that keeps our own sync honest. If you’re reading this well after that date, treat the counts as the shape of the problem rather than today’s exact numbers.
Declared filters still need runtime validation. The public probe behind the table inspects OpenAPI parameters; it does not send authenticated filter requests to an organization. A production sync should separately issue a future-dated test such as where[updated_at][gte]=2030-01-01 and assert that no rows come back. That catches an endpoint or API version that accepts a parameter without applying the semantics your sync assumes.
Deletes and merges need separate handling. A deleted person can disappear from the ordinary People collection without appearing in an updated_at window, so a purely incremental copy can accumulate ghosts. Merges do not surface as ordinary People updates either, but Planning Center exposes merger history through /people/v2/person_mergers, including the IDs kept and removed. Consume that resource where it covers your window, and keep a periodic full reconciliation for deletions and any gaps your incremental strategy cannot prove away.
Webhooks are useful but not a universal catalogue. Planning Center exposes available webhook events at /webhooks/v2/available_events. Treat that response as scoped to the authenticated organization, enabled products and permissions rather than publishing one count as universal. Inspect it with the same credentials your integration will use and record the result alongside the sync configuration.
Even where coverage exists, delivery and resource coverage still need to be tested. Add a public endpoint with retry and idempotency handling, and keep a sweep that repairs any missed window. Webhooks are a latency optimization layered on top of reconciliation, not proof of completeness by themselves.
And then it has to keep running. Token refresh. Partial failures halfway through a large product. Durable checkpoints for offset pages and high-water marks. Backfills when someone changes a definition. Monitoring that tells you the sync silently returned zero rows rather than only that it errored. None of this is hard individually. All of it is permanent.
The honest build-vs-buy
I’m not going to tell you not to build it. Sometimes it’s clearly right.
Build it if you have a developer on staff or a genuinely committed volunteer, you need something narrow, say one product feeding one report, and someone will still own it in eighteen months. A single-purpose script against /giving/v2/donations that drops a CSV in a folder every night is a perfectly good use of a Saturday.
Don’t build it if the goal is “a general reporting copy of our Planning Center data.” That’s not a script, it’s a data pipeline across eight products with mismatched incremental support, per-product versioning, and no delete events. The build is a few weeks. The maintenance is indefinite, and it lands on whoever is left when the person who wrote it moves on. The most common failure I see isn’t a sync that was built badly. It’s one that worked fine for a year and then stopped, and nobody noticed for two months because nothing errored. The numbers just quietly stopped moving.
Ask before you build. There’s also a newer option that isn’t a build at all. Planning Center ships an official AI connector, an MCP endpoint that lets Claude or ChatGPT answer questions live from five products: People, Services, Groups, Registrations and Check-Ins, scoped to the asking user’s own permissions. For a one-off question, even a cross-product one like who’s scheduled to serve Sunday but isn’t in a group, it’s the fastest answer available and costs nothing to maintain. What it doesn’t give you is a copy. It reads live and keeps nothing, so there’s no history, no trend lines, no Giving, and nothing to reconcile next quarter’s report against. It replaces the quick scripted lookup, not the sync.
The middle path most churches actually want is none of the above: they don’t want a warehouse, they want a dozen numbers that are always current.
Where we land
That maintenance is the job we took on. Parable runs this sync in production across every Planning Center product: People, Giving, Check-Ins, Groups, Services, Registrations, Calendar and Publishing, with rate pacing, incremental pulls where the endpoints genuinely support them, full reconciliation where they don’t, and probes that catch a filter silently going ignored. You connect through OAuth and the data lands in one place, joined across products and kept.
Kept is the part no CSV and no live query can do. Attendance, giving, serving and groups land on one timeline per household, with history, so who’s drifting is a question about a trend rather than a snapshot, and this quarter’s number has last year’s to stand against. If someone on your staff would rather write SQL against the result, that’s open too.
The pitch isn’t that you couldn’t build this. It’s that we already keep it running.

