Developers
Your club's data, on your terms.
A read-only REST API and signed webhooks for every Pylos club. Give your accountant a key that only sees invoices, feed results to your website, or push attendance into your own dashboards — without a spreadsheet export in sight.
Authentication
A club owner or admin creates keys under Settings → Finance → Developer. Each key has a name, a set of scopes and a per-minute rate limit. Only a hash is stored — the full key is shown once.
curl https://www.pylossystems.com/api/v1/invoices?status=Overdue&limit=50 \
-H "Authorization: Bearer pyl_…" Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit returns 429 with retry_after_seconds. A revoked key returns 401 key_revoked. Any write verb returns 405 — v1 is read-only by design.
Scopes
| read:athletes | Athletes (name, squad, age group, date of birth, competition level) |
| read:sessions | Training sessions (schedule, squad, coach, venue) |
| read:attendance | Attendance registers |
| read:invoices | Invoices and balances |
| read:payments | Payments received |
| read:results | Race / competition results |
Resources
| Endpoint | Scope | Filters |
|---|---|---|
| GET /api/v1/athletes GET /api/v1/athletes/{id} | read:athletes | squad |
| GET /api/v1/sessions GET /api/v1/sessions/{id} | read:sessions | squad |
| GET /api/v1/attendance | read:attendance | from, to, athlete_id |
| GET /api/v1/invoices GET /api/v1/invoices/{id} | read:invoices | from, to, status, billing_contact_id |
| GET /api/v1/payments GET /api/v1/payments/{id} | read:payments | from, to, billing_contact_id |
| GET /api/v1/results GET /api/v1/results/{id} | read:results | from, to, athlete_id |
| GET /api/v1/me | any | the key, its scopes, the club |
Lists are paginated with an opaque cursor: pass limit (default 50, max 200) and the next_cursor from the previous page. Dates are YYYY-MM-DD; money is a number in the club's currency. Medical information, payment card details and login data are never exposed.
{
"data": [
{
"id": "inv_…",
"invoice_number": "INV-2026-00042",
"billing_contact_id": "par_…",
"billing_contact_name": "T. van der Merwe",
"status": "Overdue",
"currency": "ZAR",
"issue_date": "2026-09-01",
"due_date": "2026-09-07",
"total_amount": 1437.5,
"amount_paid": 0,
"balance_due": 1437.5
}
],
"next_cursor": "eyJvIjoiMjAyNi0w…",
"has_more": true
}Webhooks
Register an HTTPS endpoint and choose events. We POST a JSON event with headers X-Pylos-Event, X-Pylos-Delivery-Id and X-Pylos-Signature. Respond with any 2xx within 10 seconds; anything else is retried after 1, 5, 30, 120 and 720 minutes before it is marked failed and shown in your Developer settings for a manual retry.
| invoice.created | An invoice was issued (left Draft) |
| payment.received | A payment was recorded |
| attendance.recorded | A register entry was taken or changed |
| result.imported | A race result was added (import, deck timing or manual) |
Deliveries are idempotent per event id — if you receive the same id twice, treat the second as a duplicate.
{
"id": "payment.received:pay_…",
"type": "payment.received",
"tenant_id": "…",
"created_at": "2026-09-13T18:04:11Z",
"data": {
"payment_id": "pay_…",
"billing_contact_id": "par_…",
"method": "eft_manual",
"currency": "ZAR",
"amount": 1437.5,
"received_at": "2026-09-13T18:04:09Z",
"reference": "PAY_EFT_…"
}
}Verifying a signature
The signature is t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Compute it over the raw request body — not a re-serialised object — and reject timestamps older than a few minutes.
import crypto from "node:crypto";
export function verifyPylosSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; // replay window
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
} Need write access, more events or a sandbox? Write to support@pylossystems.com — the API grows with what clubs ask for.