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:athletesAthletes (name, squad, age group, date of birth, competition level)
read:sessionsTraining sessions (schedule, squad, coach, venue)
read:attendanceAttendance registers
read:invoicesInvoices and balances
read:paymentsPayments received
read:resultsRace / competition results

Resources

EndpointScopeFilters
GET /api/v1/athletes
GET /api/v1/athletes/{id}
read:athletessquad
GET /api/v1/sessions
GET /api/v1/sessions/{id}
read:sessionssquad
GET /api/v1/attendanceread:attendancefrom, to, athlete_id
GET /api/v1/invoices
GET /api/v1/invoices/{id}
read:invoicesfrom, to, status, billing_contact_id
GET /api/v1/payments
GET /api/v1/payments/{id}
read:paymentsfrom, to, billing_contact_id
GET /api/v1/results
GET /api/v1/results/{id}
read:resultsfrom, to, athlete_id
GET /api/v1/meanythe 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.createdAn invoice was issued (left Draft)
payment.receivedA payment was recorded
attendance.recordedA register entry was taken or changed
result.importedA 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.