Webhooks / Events
Webhook events
When something happens at your school, Swum sends a signed POST to your endpoint asynchronously. Delivery can be retried and may arrive out of order.
Add an endpoint
- In the Swum app, open Settings, Developers & Integrations, Webhooks.
- Tap Add Endpoint and paste a public HTTPS address on port 443. Private, loopback, metadata/internal addresses and redirects are rejected.
- Pick the events you want and, if you like, only some pools.
- Copy the signing secret (
whsec_…). You need it to check each event.
For private connectors, use the dedicated app key and its app-owned subscription routes. General API keys and OAuth agents cannot create these subscriptions. Then use Send Test Event to verify delivery with the real signing secret. Endpoints are owner-managed and may be restricted to selected locations; records without a permitted location are not delivered to location-restricted endpoints.
What an event looks like
POST /swum/events HTTP/1.1
Host: hooks.example.test
Content-Type: application/json
Swum-Event-Id: 8b04e26e…
Swum-Event-Type: lesson.booked
Swum-Signature: t=1792936800,v1=5257a869e7ec…
{
"id": "8b04e26e…",
"type": "lesson.booked",
"created": "2026-10-25T14:00:00.000Z",
"owner_id": "owner_example",
"data": {
"family_id": "family_example",
"lesson_id": "lesson_example",
"swimmer_ids": ["swimmer_example"],
"status": "booked",
"location_id": "pool_example"
}
}id: the event's id. The same on every retry.type: what happened. Also in theSwum-Event-Typeheader.created: the source event time, in UTC.owner_id: the school owner id.data: the short version of the record. Fetch the full one from the API if you need it.
Check Swum-Signature before you trust it. Here's how.
Event types
There are 17 supported event types. Team events require team-area access on app keys; subscriber scopes and locations still apply. client.created and family.created can describe the same family creation through different emitters. Choose the event appropriate to your workflow and keep downstream effects idempotent when consuming both.
| Event | When | Data |
|---|---|---|
client.created | A school roster family is created; silent imports are skipped. | family_id, name, status; optional location_id |
lesson.booked | A real lesson becomes booked; unfinished checkout drafts are excluded. | family_id, lesson_id, swimmer_ids, status; optional location_id |
lesson.canceled | An authoritative lesson cancellation archive is created. | family_id, lesson_id, status; optional location_id |
enrollment.created | A new swimmer appears on a class occurrence’s participant record. | family_id, swimmer_id, name, class_id, instance_id, status; optional location_id |
payment.succeeded | An owner payment source transitions to successful payment. | family_id, payment_id, status, currency; optional invoice_id, amount_minor, location_id |
payment.failed | An owner payment source transitions to failed payment. | family_id, payment_id, status, currency; optional invoice_id, amount_minor, location_id |
attendance.marked | A class check-in or supported parent/staff absence is recorded. | family_id, class_id, instance_id, status; swimmer_ids for check-in, swimmer_id/name for absence; optional location_id/source |
waiver.signed | A sealed waiver signing envelope is created. | family_id, swimmer_ids, waiver_id, document_id, status; optional location_id |
skill.completed | The school progression record marks a skill complete. | family_id, swimmer_id, name, level_id, skill_id, status; optional location_id |
level.advanced | A progress level or participant-group placement advances. | family_id, swimmer_id, status; level_id/previous_level_id or group_id/previous_group_id/source; optional name/location_id |
family.created | An integration family-create mutation commits; distinct from the general client.created roster event. | family_id, swimmer_id, class_id, request_id, status, location_id; unused ids/status may be empty |
swimmer.updated | An integration swimmer-create or swimmer-update mutation commits. | family_id, swimmer_id, class_id, request_id, status, location_id; unused ids/status may be empty |
enrollment.requested | The integration records a canonical family enrollment request; this is not enrollment confirmation. | family_id, swimmer_id, class_id, request_id, status, location_id |
meet.entry.created | An individual canonical Team entry is created; family confirmation/payment are separate. | meet_id, family_id, swimmer_id, status, location_id |
meet.entry.paid | An immutable family meet request first becomes paid with canonical payment evidence. | request_id, meet_id, family_id, swimmer_ids, status, amount_minor, currency, location_id |
meet.results.imported | A meet records a new results import id; repeated writes of the same import do not emit again. | meet_id, status, location_id |
swim.best_time | A newly created non-DQ recorded swim is marked newBest; status also indicates a new cut when recorded. | time_id, meet_id, family_id, swimmer_id, course, distance, stroke, seconds, status, location_id |
Good habits
- Answer with a 2xx within 10 seconds, then do the slow work.
- Store event ids you have handled and skip repeats. A retry can arrive after a success.
- Events can arrive out of order. Use
created, or fetch the record, to get the latest.