CSA Integration
1. Overview
The Event Registration workflow integrates with Cycling South Africa (CSA) to validate rider membership and racing licenses. This validation is performed via three sequential FormField types during the registration question workflow:
| StepCode | FormField Class | Purpose |
|---|---|---|
CSI |
|
Capture CSA membership number and identification details — only from riders who are not already resolvable (see Rule: Conditional prompt) |
CSM |
|
Validate CSA membership status via CSA external API |
CSL |
|
Validate CSA racing license via CSA external API |
These steps form a dependent chain: CSI captures the identifier, CSM validates membership using that identifier, and CSL validates the racing license.
2. ProcessData Flow
The CSA steps follow the standard ProcessData staging pattern. Each step reads data from prior steps and stores its result:
| Step | Reads From | Stores |
|---|---|---|
CSI |
(user input) |
CSA membership number, ID number, ID country |
CSM |
CSI ProcessData (CSA number or ID number) |
Membership validation result, CSA member ID, membership status |
CSL |
CSI ProcessData (CSA number or ID number) |
License validation result, membership status, license status |
Both CSM and CSL gate form progression — the participant cannot advance to the next question if validation fails, unless an alternative action is chosen (day license, defer, exempt).
3. CSA API
The CSA API is hosted by Cycling South Africa at cyclingsa-events.co.za. It provides two endpoints for checking a rider’s membership and racing license status.
Official API documentation: https://documenter.getpostman.com/view/28907025/2sAYdkFoEm
|
This section documents the v1 API surface, retained for the legacy WordPress plugin and historical batch back-fills (club / team-roster enrichment). admin-service’s |
3.1. Authentication
Authentication is via an API key passed in the JSON request body (not as an HTTP header).
| Parameter | Description |
|---|---|
|
A unique API key assigned to the organisation. Must be included in every request body. |
The API key must be stored as an application property (csa.api.key), never hardcoded.
3.3. Endpoints
3.3.1. Check Membership Status
Checks whether a rider has an active CSA membership on a given date.
URL: ?c=Member&fn=checkMembershipStatus
Method: POST
Content-Type: application/json
Request body:
{
"key": "<API_KEY>",
"idNumber": "8501015009086",
"date": "2026-06-15"
}
| Field | Type | Description |
|---|---|---|
|
String (required) |
API authentication key |
|
String (conditional) |
South African ID number. Much better suited for ZA citizens as it is a unique national identifier. Cannot be used for foreign riders. |
|
String (conditional) |
CSA’s internal member reference number. Can be used for any rider (local or foreign), but |
|
String (optional) |
Date to check in |
Either idNumber or csaId must be provided. When the person’s ID country is ZA and an ID number is available, prefer idNumber as it is the stronger identifier. For foreign riders, csaId is the only option.
|
Response body:
{
"ok": true,
"memberFound": true,
"active": true,
"date": "2026-06-15"
}
| Field | Type | Description |
|---|---|---|
|
Boolean |
|
|
String |
Error description. Only present when |
|
Boolean |
|
|
Boolean |
|
|
String |
The date that was checked (YYYY-MM-DD). |
3.3.2. Check Racing License Status
Checks the rider’s membership status AND racing license status in a single call. This is the more comprehensive endpoint.
URL: ?c=Member&fn=checkRacingLicenceStatus
Method: POST
Content-Type: application/json
Request body: Same structure as membership check.
{
"key": "<API_KEY>",
"idNumber": "8501015009086",
"date": "2026-06-15"
}
Response body:
{
"ok": true,
"memberFound": true,
"memberCsaId": "12345",
"membershipStatus": "ACTIVE",
"licenceStatus": "ACTIVE",
"date": "2026-06-15"
}
| Field | Type | Description |
|---|---|---|
|
Boolean |
|
|
String |
Error description. Only present when |
|
Boolean |
|
|
String |
The CSA ID of the cyclist (returned by CSA, useful for cross-referencing). |
|
String |
Membership status on the given date (see status codes below). |
|
String |
Racing license status on the given date (see status codes below). |
|
String |
The date that was checked (YYYY-MM-DD). |
3.4. Status Codes
3.4.1. Membership Status Values
| Value | Meaning |
|---|---|
|
The cyclist has an active membership on the checked date. |
|
The cyclist’s membership has expired. |
|
The cyclist’s membership is suspended. |
|
The cyclist is not a CSA member. |
|
The cyclist is not a CSA member (alternative phrasing of |
3.4.2. License Status Values
| Value | Meaning |
|---|---|
|
The cyclist has an active racing license on the checked date. |
|
The license is inactive. This occurs when the cyclist has a racing license but their membership has expired — the license becomes inactive because it depends on active membership. |
|
The cyclist’s racing license has been suspended. |
|
The cyclist does not have a racing license. |
|
The cyclist does not have a racing license (alternative phrasing of |
|
The |
3.5. Key API Observation
The checkRacingLicenceStatus endpoint returns both membership and license status in a single response. The legacy implementation exploits this by calling only checkRacingLicenceStatus for both the membership check and the license check. This avoids making two separate API calls.
The new Java implementation no longer uses these v1 function endpoints. See CSA API V2 (current — used by admin-service) below, which the CsaApiClient now targets.
4. CSA API V2 (current — used by admin-service)
CSA introduced a v2 API that supersedes the v1 surface above. admin-service’s CsaApiClient uses v2. The same API key works on both APIs unchanged — only the transport and response shape differ.
4.1. Base URL and Endpoints
https://www.cyclingsa-events.co.za
| Endpoint | Method | Purpose |
|---|---|---|
|
|
Single call returning membership, racing-licence, day-licence, UCI and geographic/club data. |
|
|
Create a day licence for a rider at a CSA event. |
4.2. Authentication
For /api/v2/members/status the API key is sent in the Authorization HTTP header (v1 sent it in the body). For /api/v2/day_licenses the key is sent in the request body (key), as in v1 — this asymmetry is intentional on the CSA side.
4.3. Member Status Request
POST /api/v2/members/status with Authorization: <API_KEY> and Content-Type: application/json. Field names are snake_case.
{
"csa_id": "12345",
"id_number": "8501015009086",
"csa_event_id": "<event-uuid>",
"date": "2026-06-15"
}
| Field | Type | Description |
|---|---|---|
|
String (conditional) |
SA ID number. Preferred for ZA citizens (stronger identifier). |
|
String (conditional) |
CSA member reference. The only option for foreign riders. |
|
String (optional) |
CSA’s id for the event the check is for, taken from |
|
String (optional) |
Check date (YYYY-MM-DD). Defaults to today. Use the event start date. |
At least one of id_number / csa_id must be provided.
4.4. Member Status Response
{
"member_found": true,
"member_csa_id": "12345",
"membership_status": true,
"racing_license_status": true,
"day_license_status": false,
"csa_id_match_member_record_id_number": true,
"uci_id": "10017823025",
"uci_code": "RSA12345",
"uci_category": "Master Men 45-49",
"date": "2026-06-15",
"country": { "id": "...", "name": "SOUTH AFRICA" },
"region": { "id": "...", "name": "Gauteng" },
"city": { "id": "...", "name": "..." },
"club": { "id": "<uuid>", "name": "..." }
}
| Field | Type | Description |
|---|---|---|
|
Boolean |
Rider exists on the CSA system. |
|
String |
The rider’s CSA ID. Persist this for future lookups when the rider was found by SA ID. |
|
Boolean |
|
|
Boolean |
|
|
Boolean |
Whether a day licence already exists / is applicable for the rider. |
|
Boolean |
Trust signal. |
|
String |
UCI enrichment. |
|
Object |
Geographic and club affiliation. The nested |
4.5. Deriving the internal status from v2 booleans
v2 collapses v1’s string status codes into booleans. admin-service derives its internal CsaCheckStatus as follows:
| Condition | Internal status |
|---|---|
|
|
|
membership |
|
membership |
|
licence |
|
licence |
|
licence |
(HTTP / connection error) |
|
4.6. Day Licence Creation
POST /api/v2/day_licenses with the key in the body:
{ "key": "<API_KEY>", "csa_id": "12345", "event_id": "<event-uuid>" }
Returns the created day-licence id, validity dates, and price. The day licence should be issued after the participant’s payment is confirmed, not at form completion.
event_id is the csa_event_id of the participant’s event (ADO #877). There is no global fallback: CSA’s event id identifies one specific race, so a deployment-wide value would issue every licence against the same event. An event with no CSA event id gets no licence — see the lifecycle in Rule: Day License.
4.7. V1 to V2 mapping
| Aspect | V1 | V2 |
|---|---|---|
Endpoint(s) |
|
single |
Auth |
|
|
Field case |
camelCase ( |
snake_case ( |
Status type |
Strings ( |
Booleans ( |
Enrichment |
none |
UCI ids, country/region/city/club, ID-number match flag |
5. Java Implementation: CsaApiClient
The CsaApiClient Spring service in admin-service implements the CSA v2 integration (see CSA API V2 (current — used by admin-service)). It supports a mock mode (csa.api.mock=true, the default) for development/testing and real v2 calls when mock=false. The request factory is pinned to Apache HttpClient5 to avoid a JDK HttpClient connection-reuse race.
5.1. Configuration Properties
| Property | Default | Description |
|---|---|---|
|
CSA API host (the client appends |
|
|
(none — required) |
API authentication key ( |
|
|
HTTP request timeout in milliseconds |
|
|
Use mock responses for development/testing |
There is deliberately no csa.api.csa-event-id. CSA’s event id belongs to each event and is set on
event.csa_event_id — for now over REST with PATCH /api/events/{id} (csaEventId), until an event
setup screen exists (ADO #877). The former global property was blank in every environment and has
been removed.
5.2. Implementation Notes
-
HTTP client: Use Spring
RestTemplateorWebClientto POST JSON to the CSA API. -
Request construction: Build a JSON object with
csa_idorid_number, anddate(snake_case). Addcsa_event_idonly when the event being registered for has one (event.csa_event_id); omit it when the event has none and in membership registration, which has no event. The API key goes in theAuthorizationheader for/members/status(in the body for/day_licenses). -
SSL: The legacy implementation disables SSL verification. The new implementation must NOT disable SSL verification. If certificate issues arise, add the CSA certificate to the Java truststore.
-
Error handling:
-
cURL/HTTP errors → return
CsaMembershipResponse.offline()orCsaLicenseResponse.offline() -
ok=falsein response → log the error message, return offline status -
memberFound=false→ returnNOT_FOUNDstatus
-
-
Lookup preference: When the person’s ID country is
ZAand an ID number is available, sendidNumber(stronger identifier). For foreign riders, sendcsaId(the only option). Both can be used for any rider, butidNumberis preferred for ZA citizens.
5.3. Suggested CsaApiClient Method Signatures
/**
* Check membership status using the checkMembershipStatus endpoint.
* Use when only membership status is needed.
*/
public CsaMembershipResponse checkMembership(String idNumber, String csaId, LocalDate date);
/**
* Check both membership and license status using checkRacingLicenceStatus.
* Preferred method -- returns both statuses in one call.
*/
public CsaLicenseResponse checkLicense(String idNumber, String csaId, LocalDate date);
6. Validation Workflow
6.1. Business Rules Hierarchy
CSA requirements are determined at two levels, with category overriding event:
6.1.1. Rule: License Requires Membership
When a racing license is required, CSA membership is always implicitly required. A rider cannot hold an active license without an active membership (CSA enforces this — an expired membership causes the license status to become INACTIVE).
6.1.2. Rule: Requirement Inheritance
Requirements cascade: Series → Event → Category. A category-level setting overrides the event-level setting. An event-level setting overrides the series-level setting. If no value is set at a level, it inherits from the parent.
6.1.3. Rule: Strict vs Lenient Registration
-
Strict (
strictRegistration=true): The rider must have valid membership/license at the time of registration. The form blocks progression if validation fails. -
Lenient (
strictRegistration=false): The rider can register even without valid status. The compliance state is recorded and can be enforced closer to event day.
6.1.4. Rule: Day License
When a rider does not hold a valid license, they may purchase a day license — unless the event explicitly disallows it (dayLicenseDisallow=true).
Lifecycle of a day licence:
-
The decision is recorded per person as a
DAY_LICENSEaction in the CSM/CSLComplianceAnswer(or implied by choosing "None" at CSI), and collapsed at form completion intoEventParticipant.complianceStatus = DAY_LICENSE. -
A second
OrderLineItemis created on the sameEventParticipant, billed against the day-licence product resolved asEvent.productDayLicensefalling back toSeries.productDayLicense. If neither is configured, no line item is added and a warning is logged. -
In the WooCommerce payload the day-licence line is a separate
eventParticipant[]entry (same participant id, day-licence product) that deliberately omitscategoryandraceTypes— the plugin skips blank meta values, so the line renders on the order, invoice and e-mail as the day-licence product name with the rider’s name and date of birth only. Each day licence is a named, quantity-1 line item, which distinguishes day licences issued to multiple people on one order. -
After the order is paid,
DayLicenseIssuanceServicecalls CSA v2POST /api/v2/day_licensesagainst the participant’s event’scsa_event_id— gated by thecsa.api.day-license-issuance-enabledproperty (default off; riders are billed regardless, issuance fires only when enabled). -
The outcome is recorded on the participant (ADO #875, #876). An issued licence stores CSA’s day-licence id and the issue time. A licence that was not issued — no CSA number on file, no CSA event id on the event, CSA rejected the request, the call failed, or issuance switched off — raises the admin review flag (
adminReviewReason = DAY_LICENSE_NOT_ISSUED) and records the cause indayLicenseFailure, so billed-but-unissued riders are listed byGET /api/event-participants?adminReviewReason.equals=DAY_LICENSE_NOT_ISSUED.
Declining the day licence (remove-on-cancel): when the buyer picks "None" at CSI,
the portal explains that a Day Licence will be added to the order and offers three
choices: Continue with Day Licence, Remove <name> from registration (they can
register again later on their own), or Cancel. Choosing removal submits the CSI answer with
action: "REMOVE"; the backend whitelists exactly this action, marks the person
removed (mirroring the CSM/CSL REMOVE handling), skips them for all remaining steps
and excludes them from EventParticipant and order creation. Re-selecting CSA/UCI
before submitting the step clears the pending removal.
Cancel is a no-op: the toggle keeps whichever option was selected before "None" was clicked, and any partially typed CSA/UCI number is preserved. It exists because the dialog is a hard modal (no close button, no escape key, no backdrop dismiss) — without it, merely clicking "None" to read what it means forces the buyer into one of two consequential outcomes with no way back.
6.2. Identification (CSI)
CSI exists solely to obtain an identifier for the downstream CSM/CSL lookups. It is not shown unconditionally — prompting a rider who is already identifiable is noise, and worse, it invites them to answer "None" and be billed a day licence they do not need.
6.2.1. Rule: Conditional prompt
A rider is resolvable when any of the following holds:
| Resolvable by | Condition |
|---|---|
SA ID number |
The rider’s identity number passes SA ID checksum validation
( |
Persisted UCI ID |
A |
Persisted CSA ID |
A |
Riders who are not resolvable are prompted for a CSA number, a UCI number, or "None".
6.2.2. Rule: Step visibility
Resolvability gates the step, not just the person, because FormPersonMapper omits
skipped people from the response payload entirely:
-
No rider needs input — the whole CSI step is skipped; the form advances straight to CSM. This is the common single-rider case.
-
At least one rider needs input — CSI renders, and resolvable riders stay on the screen in a read-only state showing how they were resolved (e.g. "Verified via SA ID number — nothing needed here"). They are not hidden.
Keeping resolvable riders visible is deliberate and mirrors CSM/CSL, which keep CSI="None" riders visible with a status banner. Silently dropping a person from a multi-rider screen reads as a bug to the buyer ("where did Alex go?").
6.2.3. Rule: Resolved answers are not "None"
A resolvable rider’s pre-filled answer carries an explicit resolvedBy discriminator:
{"csaId":"","uciId":"","resolvedBy":"SA_ID"}
This matters because {"csaId":"","uciId":""} — blanks with no discriminator — is the
wire shape for "None", which EventFormController.isCsiNone reads as opted into a day
licence. Without the discriminator a rider resolved by SA ID would be indistinguishable
from one who declined to supply a number, and would be billed a day licence despite
holding valid membership.
isCsiNone therefore returns false whenever resolvedBy is set, and additionally
returns false when the rider has a usable SA ID — a "None" answer must never suppress
a lookup that would have succeeded on the SA ID alone.
6.3. Membership Validation (CSM)
The CSM step calls the CSA API to check whether the rider’s membership is current and valid for the event.
Validation logic:
-
Read the CSA ID (or ID number) from the CSI step’s ProcessData
-
Call
checkRacingLicenceStatuswith the ID and the event start date as the check date -
Map the
membershipStatusresponse to an internal status code -
Determine available actions based on the status
Status mapping:
CSA API membershipStatus |
Internal Status | Available Actions |
|---|---|---|
|
|
Proceed (auto-advance) |
|
|
Renew membership, purchase day license (if allowed), defer, remove person |
|
|
Remove person from registration |
|
|
Purchase day license (if allowed), defer, remove person |
(API unreachable) |
|
Defer (allow registration, check later) |
When status is ACTIVE: The CSM step auto-fills the answer as valid and can be configured to auto-advance (skip=true) so the user sees no interruption.
6.4. License Validation (CSL)
The CSL step calls the CSA API to check the rider’s racing license status.
Validation logic:
-
Read the CSA ID from CSI ProcessData (same as CSM)
-
Call
checkRacingLicenceStatuswith the ID and the event start date -
Map the
licenceStatusresponse to an internal status code -
Determine available actions based on the status
Status mapping:
CSA API licenceStatus |
Internal Status | Available Actions |
|---|---|---|
|
|
Proceed (auto-advance) |
|
|
Renew membership first (license becomes active when membership is renewed), defer, remove person |
|
|
Remove person from registration |
|
|
Purchase day license (if allowed), defer, remove person |
(API unreachable) |
|
Defer (allow registration, check later) |
|
The |
6.5. User Actions on Validation Failure
When CSM or CSL validation fails, the user is presented with action options:
| Action | Description |
|---|---|
PROCEED |
Only available when status is |
RENEW |
Directs the rider to renew their CSA membership. The rider must renew externally and then retry. |
DAY_LICENSE |
Adds a day license to the order. Only available if the event allows day licenses. A day license covers the rider for the specific event date only. |
DEFER |
Allows the registration to continue with a compliance status of |
REMOVE |
Removes this person from the registration. Used when the rider cannot or will not resolve the compliance issue. |
7. Caching Strategy
The legacy implementation uses a caching strategy to avoid redundant API calls:
-
Active status: Cache the result until the event start date. An active membership/license checked for the event date will not change, so the result is stable.
-
Non-active status: Cache for 5 minutes only. The rider may renew their membership at any time, so we retry after a short interval.
-
Within 1 minute: Always return the cached result to prevent rapid repeated calls (e.g., page refreshes).
The new implementation should use Spring’s @Cacheable with a TTL-based eviction policy. Cache keys should include the CSA ID and the check date.
8. Admin-Service Wrapper API
The admin-service exposes its own REST endpoints that wrap the CSA API. Frontends (registration-portal, admin-ui) call the admin-service endpoints instead of calling CSA directly. This avoids CORS issues and keeps the API key server-side.
8.1. Endpoint: Check Membership and License
POST /api/csa/check
Request body:
{
"idNumber": "8501015009086",
"csaId": "12345",
"date": "2026-06-15"
}
| Field | Type | Description |
|---|---|---|
|
String (optional) |
South African ID number. Preferred for ZA citizens. |
|
String (optional) |
CSA member ID. Used for foreign riders or when ID number is unavailable. |
|
String (optional) |
Date to check (YYYY-MM-DD). Defaults to current date if omitted. |
At least one of idNumber or csaId must be provided.
|
Response body:
{
"ok": true,
"memberFound": true,
"memberCsaId": "12345",
"membershipStatus": "ACTIVE",
"licenceStatus": "ACTIVE",
"date": "2026-06-15"
}
The response DTO mirrors the CSA checkRacingLicenceStatus response. For now the admin-service wrapper shares the same DTO structure as the upstream CSA API, keeping the integration simple. If the wrapper needs to diverge (e.g., enriching with internal compliance data), the DTOs can be separated later.
8.2. Endpoint: Check Membership Only
POST /api/csa/check-membership
Same request body. Returns the simpler membership-only response (mirrors checkMembershipStatus).
{
"ok": true,
"memberFound": true,
"active": true,
"date": "2026-06-15"
}
8.3. Design Rationale
-
No CORS: Frontends call admin-service on the same origin. Admin-service calls CSA server-side.
-
API key security: The CSA API key is never exposed to the browser.
-
Caching: Admin-service can cache CSA responses (see Caching Strategy) to reduce external API load.
-
Resilience: Admin-service handles timeouts, retries, and circuit breaking. Frontends get a consistent error response regardless of CSA API behaviour.
-
Shared DTOs (for now): The admin-service response mirrors the CSA API response structure. This avoids unnecessary mapping layers while the integration is straightforward. If admin-service needs to enrich or transform the response in future, the DTOs can be separated without breaking the upstream CSA contract.
9. ComplianceStatus on EventParticipant
The EventParticipant entity has a complianceStatus field (enum ComplianceStatus) that records the outcome of CSA validation:
| Value | Meaning |
|---|---|
|
Membership and license checks passed with |
|
The rider is exempt from CSA requirements (manually set by admin). |
|
The rider purchased a day license for this event. |
|
The rider deferred resolution. Must be resolved before event day. |
|
Checks have not yet been completed (initial state). |
This field is set by the CSM and CSL FormFields when saving their ProcessData, and is also available for onDone() to read when creating the final EventParticipant entity.
10. Configuration
Events that require CSA validation include CSI, CSM, and CSL steps in their ProcessDefinition. Events that do not require CSA validation simply omit these steps — the ProcessDefinition is configurable per event.
The CSA requirement flags on Event and EventCategory entities control the business rules (see Business Rules Hierarchy).
11. Source Code
11.1. Legacy WordPress Implementation (Reference)
| File | Purpose |
|---|---|
|
API client — HTTP calls to CSA endpoints via cURL |
|
Business logic — requirement hierarchy, status mapping, caching, validation rules |
|
DTO for |
|
DTO for |
|
UI for individual rider CSA status and day license purchase |
|
Review page showing all entrants' CSA status |
11.2. New Java Implementation
| File | Purpose |
|---|---|
|
Spring service — CSA v2 API client (mock + real) |
|
Enum: ACTIVE, EXPIRED, OFFLINE, NOT_FOUND, SUSPENDED |
|
DTO for membership check response |
|
DTO for license check response |
|
RuntimeException for API failures |
|
v2 wire DTOs for |
|
admin-service wrapper response for |
|
|
|
v2 day-licence creation DTOs for |
|
CSI step — captures CSA ID and UCI ID |
|
CSM step — validates membership, presents action options |
|
CSL step — validates license, presents action options |
|
JSON answer for CSI step |
|
JSON answer for CSM/CSL steps |
|
Enum: VALID, EXEMPT, DAY_LICENSE, DEFERRED, PENDING |
|
Entity with |
|
Age calculator per CSA regulation (age on 31 December of current year) |