For developers

WorshipPro API

Read your church's songs, service plans, roster, events and (with separate consent) people from your own website, spreadsheet, n8n or Zapier. API v1 is read-only.

Your first call

  1. A church administrator opens Church settings → Integrations → API tokens and creates a token: name, scopes and validity (30–365 days).
  2. The full token (wp_live_…) is shown only once. Keep it in your integration's secret store, not in website code or the browser.
  3. Send it in the Authorization: Bearer header. Start by inspecting the token — the response shows the church and the scopes that work.
curl -H "Authorization: Bearer $WORSHIPPRO_TOKEN" \
  https://worshippro.net/api/v1/token

The API has no CORS: the token must never reach your visitors' browsers. Fetch the plan for your website server-side or use the iCal calendar.

Scopes

A token works within the intersection of its scopes ∩ the current permissions of the person who created it ∩ the modules enabled in the church. If the creator loses their role, the token stops working or works more narrowly.

songs:read Songs
Lyrics, chords, keys and arrangements (no files).
plans:read Service plans
Plan items and times, without the roster and private notes.
teams:read Teams and roster
Teams, positions and who serves in plans (names only, no contact details).
events:read Events
Public events from the church calendar, and rooms.
people:read People (contact details)
Name, email, phone, status, household and tags. Never sensitive data or notes.

The people scope requires permission to manage the people directory and a data processing agreement with the recipient. Sensitive data (health, birth and baptism dates, custom fields), notes, consents and children never leave through the API.

Rate limits

  • 120 units per minute and 5000 per hour per token
  • 300 units per minute for the whole church
  • A regular request costs 1, a list with the q search 5, events in a from/to window 2.
  • Every response carries the RateLimit-Policy and RateLimit headers. When you exceed a limit you get 429 with a Retry-After header.

Pagination and sync

Lists return up to 100 records (limit) and nextCursor. Pass it as cursor until you get null. For incremental sync remember the newest updatedAt and query with updatedSince; fetch deletions from /deleted?since=.

Every error has the same shape; the message field follows the Accept-Language header:

{
  "error": {
    "code": "insufficient_scope",
    "message": "…",
    "required": "people:read",
    "reason": "token_scope"
  },
  "requestId": "…"
}

Examples

Plans for next Sunday

curl -H "Authorization: Bearer $WORSHIPPRO_TOKEN" \
  "https://worshippro.net/api/v1/plans?from=2026-10-11&to=2026-10-11"

Sync songs changed since last time

curl -H "Authorization: Bearer $WORSHIPPRO_TOKEN" \
  "https://worshippro.net/api/v1/songs?updatedSince=2026-10-01T00:00:00Z&limit=100"

# next page:
curl -H "Authorization: Bearer $WORSHIPPRO_TOKEN" \
  "https://worshippro.net/api/v1/songs?updatedSince=2026-10-01T00:00:00Z&limit=100&cursor=<nextCursor>"

curl -H "Authorization: Bearer $WORSHIPPRO_TOKEN" \
  "https://worshippro.net/api/v1/deleted?since=2026-10-01T00:00:00Z"

Events in October (recurring events expanded)

curl -H "Authorization: Bearer $WORSHIPPRO_TOKEN" \
  "https://worshippro.net/api/v1/events?from=2026-10-01&to=2026-10-31"

Webhooks

Instead of polling the API, your integration can receive notifications. A church administrator adds an endpoint (public https only) in Church settings → Integrations → Webhooks and picks the events. Messages are short and carry no personal data, only ids. Fetch the details through the API with your token.

Every message is a POST with JSON in the same envelope. The type field says what happened, and data carries only the fields listed below.

{
  "v": 1,
  "id": "evt_2f6c0d8e4b1a4c7f9e3d5a6b7c8d9e0f",
  "type": "donation.paid",
  "createdAt": "2026-10-11T08:40:00.000Z",
  "churchId": "8b1e…",
  "data": {
    "donationId": "5e0b…",
    "amount": 5000,
    "currency": "PLN",
    "fundId": "a4f7…",
    "paidAt": "2026-10-11T08:40:00.000Z",
    "method": "online"
  }
}

Events

plan.updated

A service plan was created, changed (including items and times), published or deleted. A series of edits goes out as one message 10 minutes after the last one. Plan templates send no events.

data fields: planId, updatedAt

Fetch GET /api/v1/plans/{planId} (plans:read scope). 404 means the plan was deleted; it is also listed in /deleted.

form.submission_created

Someone sent a sign-up form (public, no account needed) or an organizer entered a submission. No name, email or answers.

data fields: formId, submissionId, createdAt

View the submission in WorshipPro (Sign-ups & forms). Reading answers through the API is not available yet.

donation.paid

A payment is confirmed: an online gift once the payment provider confirms it, a donation recorded by the treasurer, or an offering item once two people close the count. In-kind gifts send no event.

data fields: donationId, amount, currency, fundId, paidAt, method

Amount in the smallest currency unit (5000 = 50.00 PLN), fund of the largest part of the gift (null means no fund), method: cash, transfer, online or card. No donor details; finance data is not available through the API.

An endpoint only receives events the person who added it can access today (plans, people with edit rights, accounting). If they lose access, those messages are skipped.

Delivery and retries

  • Respond with a 2xx status within 10 seconds. Redirects are not followed and the response body is ignored.
  • After a failure we retry after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 24 h (8 attempts in total). Retry-After on 429 and 503 is honored (up to 24 h).
  • The same message can arrive more than once. The webhook-id header stays the same across attempts, so use it to drop duplicates.
  • A 410 Gone response, 5 days of failures only, or 3 messages in a row that failed every retry turn the endpoint off. The administrator gets an email and can turn it back on (and resend the messages that failed in the last 7 days).
POST /your-endpoint
content-type: application/json
webhook-id: msg_…
webhook-timestamp: 1791707400
webhook-signature: v1,K5oZfz…=

Verifying the signature

Messages are signed according to Standard Webhooks, so you can use the ready-made "standardwebhooks" library (many languages) with the whsec_… secret. Without a library:

  1. Reject the message if webhook-timestamp differs from your clock by more than 5 minutes.
  2. Compute HMAC-SHA256 of: webhook-id, a dot, webhook-timestamp, a dot and the raw body. The key is the base64-decoded bytes after the whsec_ prefix.
  3. Compare the result (base64) in constant time with every v1 signature in the webhook-signature header. For 24 h after a secret change the header carries two signatures: the new one and the old one.

The signature covers the body exactly as received, before you parse it as JSON.

Node.js

import crypto from "node:crypto";

// secret: "whsec_…" (shown once in Church settings → Integrations → Webhooks)
// rawBody: the request body exactly as received (string, before JSON.parse)
function verifyWorshipProWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const ts = Number(headers["webhook-timestamp"]);
  if (!id || !Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
  return String(headers["webhook-signature"] ?? "").split(" ").some((part) => {
    const [version, sig] = part.split(",");
    const got = Buffer.from(sig ?? "", "base64");
    return version === "v1" && got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}

Resource reference

Loading the reference…

    API for developers — WorshipPro