Growth LMS Integration API (Developers)
Test authentication, Attendance, map partner members, check course eligibility, and report attendance to Semis Growth LMS using our REST API.
The Semis Partner API connects external member and attendance systems to Growth LMS. Follow this guide in order: authenticate → preview a member sync → apply/review → read back → optionally check eligibility → optionally report attendance. Use it to submit attendance from instructor-led training, employee development events, or other organisational learning programmes. Stable external identifiers support consistent member matching and attendance reconciliation.
Before you start
You need an authorised organisation integration key and a server-side HTTPS client. Use an owner-approved test member and event. The public URL below is production, not a disposable sandbox. Replace all illustrative IDs, emails, and timestamps with your approved test data before writing. This reference is for integration developers. You need an authorised organisation integration key and a server-side application that can make HTTPS requests.
Base URL and authentication
https://semis.reispar.com/api/v1/partner/
The public Semis domain proxies this API to the backend. A separate backend hostname
is not required. Use the full API path, not the homepage or the JWT-authenticated
/api/v1/integrations/ settings endpoints. If Semis supplies a separate test
environment, use that environment's hostname and key consistently.
Responses are plain JSON. Authenticate every request using the full secret
(starting with sk_), not the integration ID or the abbreviated display prefix:
Authorization: Bearer <FULL_INTEGRATION_SECRET>
An Owner or Co-Owner creates a key in Organisation Settings → Integrations, selecting the required permissions. The secret is shown once at creation/rotation. A key belongs to one organisation, not one member. The key selects the organisation automatically; partner URLs do not take an organisation ID.
Protect the secret
Keep the key in a server-side secret manager. Never commit it, expose it in browser or mobile client code, or include it in screenshots and support messages. Ask an owner to rotate a leaked key. Saving a key in your app does not establish a connection: an actual authenticated request must reach Semis.
| Scope | Settings permission | Allows |
|---|---|---|
members:read | Read members | Look up a member by external ID, list the organisation's departments, and list members not yet linked |
members:write | Sync members | Create/update members and preview syncs |
completions:read | Read eligibility | Check course-completion requirements |
attendance:write | Record attendance | Report attendance |
Where the identifiers come from
| Identifier | Source | Meaning |
|---|---|---|
external_member_id | Your third-party system | Its stable person ID, for example emp_5567 |
external_event_id | Your third-party system | Its stable event ID, for example evt_1024 |
employee_id | Your organisation/HR system, optionally | A separate field; not automatically the external member ID |
| Semis membership ID | Semis | An internal ID; not what the partner lookup URL expects |
Semis stores external_member_id on the member during sync. Creating a key does not
map existing members. A manually added member may have no external ID yet; the API
mapping does not require a visible column in the regular Members screen.
Use the same external member ID for sync, lookup, eligibility, and attendance. It must be unique within the organisation, including across its integrations. Do not generate a new ID for every call. URL-encode IDs in paths and query strings.
Step 1: prepare your test client
For the complete workflow, request all four scopes. For member sync only, request
members:read and members:write.
The examples use Bash, with SEMIS_API_KEY already populated securely in the
environment:
SEMIS_API_BASE="https://semis.reispar.com/api/v1/partner"
Postman: choose Authorization → Bearer Token and enter the full secret. For writes, choose Body → raw → JSON. Match the method and URL in each step.
Windows PowerShell: use curl.exe, not the curl alias. Bash backslash line
continuations do not work in PowerShell. For example, with the secret already in
$env:SEMIS_API_KEY:
curl.exe -sS -w "\nHTTP_STATUS=%{http_code}\n" "https://semis.reispar.com/api/v1/partner/members/connection-check/" -H "Authorization: Bearer $env:SEMIS_API_KEY"
Opening the URL in a browser address bar does not attach the Bearer header.
Step 2: test authentication without changing members
There is currently no dedicated connection-check endpoint. This is an ordinary
member lookup using the deliberately unused external ID connection-check:
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
"$SEMIS_API_BASE/members/connection-check/" \
-H "Authorization: Bearer $SEMIS_API_KEY"
If that ID does not exist, the expected result is HTTP 404:
{
"detail": "Member not found."
}
This exact JSON 404 means authentication, organisation entitlement, and Read members permission passed; only the member lookup failed. It does not mean the key is unlinked or broken. If that external ID exists, expect 200 with its details instead.
Repeat without the header:
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
"$SEMIS_API_BASE/members/connection-check/"
Expected: HTTP 401:
{
"detail": "Authentication credentials were not provided."
}
An HTML 404 or login page is not the expected partner JSON response. Check the URL, environment, and proxy configuration in that case.
Authentication is not member mapping
This test proves the key resolves to an eligible organisation, not that a member has been synced or that it is the organisation you intended. Confirm the key with its issuing owner before writing. Do not recreate a valid key because the dummy member lookup returned 404.
Step 3: preview a member mapping
Use your system's stable person ID and the person's actual membership email. Semis matches by external ID first, then by membership email inside the key's organisation.
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
-X POST "$SEMIS_API_BASE/members/?dry_run=true" \
-H "Authorization: Bearer $SEMIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_member_id": "emp_5567",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Musa"
}'
A successful preview returns 200, dry_run: true, would_block, and the proposed
changes. If would_block is true, the actual sync would require review.
The preview does not apply the mapping or create a pending-review item. Request logs and Last used may still update. A preview is not a guarantee that the actual write will pass every database validation; inspect the actual response too.
Step 4: apply the mapping and resolve review
This step writes data. Repeat the same POST and JSON from step 3, using:
https://semis.reispar.com/api/v1/partner/members/
Remove ?dry_run=true and any dry_run body flag.
| Response | Meaning and next step |
|---|---|
201, created: true | A new member was created. Confirm this was intended. |
200, created: false | An existing member was mapped/updated. |
409, status: "pending_review", with review_id | An Owner, Co-Owner, or Admin must review it in Integrations → Sync activity. |
Other 400 or 409 | Inspect validation/conflict details. Not every 409 is a review request. |
An email is required to create a new member. Matching a protected existing account only by email can require approval to prevent silently taking over that record. Do not repeatedly resend a blocked request. A pending or rejected review is not a completed mapping.
Example applied response:
{
"external_member_id": "emp_5567",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Musa",
"employee_id": null,
"job_title": "",
"departments": ["Engineering"],
"department_details": [
{ "department_id": "5b0f6a52-3c1e-4f0e-9f57-0d6f1c2a7e11", "name": "Engineering" }
],
"created": false
}
Note: job_title can be a custom field within your Semis Growth Organisation, as this particular field name is not created by default.
Step 5: read back the mapped member
After a successful write or approved review:
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
"$SEMIS_API_BASE/members/emp_5567/" \
-H "Authorization: Bearer $SEMIS_API_KEY"
Expected: 200, with the correct external ID, email, and profile fields. The read
response does not include the sync-only created flag.
A 404 here requires checking the exact ID, key/organisation/environment, review status, and whether the member was deleted. Do not substitute an email, employee ID, or Semis membership UUID unless that exact value was synced as the external ID.
A successful new-member sync starts Semis onboarding. When the external ID and
email do not match an existing Growth membership, Semis creates the membership with
status invited, reserves a licensed seat, creates a Semis invitation, and queues the
invitation email after the database transaction commits. The employee is not active
until they complete the normal invitation flow. Their email, first name, and last name
are stored on the membership; the current registration screen does not prefill profile
names from this sync.
The API response confirms that Semis accepted the sync and created the invitation. It does not prove inbox delivery. Email delivery is asynchronous and can be delayed by the configured invitation-delivery rate limit or retried after a mail-provider failure. Final invitation-delivery failures are recorded in durable trailing 15-minute batches, separately from member-sync outcome summaries. An accepted sync or queued invitation must not be treated as proof of final email delivery. Updating an already synced member does not create or send another invitation.
New members require both an active subscription and an available licensed seat. A
single-member sync returns 402 when no seat is available. A batch sync continues
with its other members and reports that item as no_seats; free or purchase a seat and
retry only that item. Do not tell an employee that they can access Growth merely because
the external system accepted their member record.
Member-sync-only integrations can stop here. Send future updates using the same external ID. Mapping does not itself enrol the person in a course, complete training, or register them for a Semis event.
Syncing the same member again
You can sync a member as often as you like. What happens depends on how Semis matches them:
| You send | Semis does |
|---|---|
The same external_member_id as before | Matches that member and updates the fields you send: first_name, last_name, employee_id, job_title, department. Fields you leave out are kept. No new invitation is sent and no new seat is used. Returns 200, created: false. |
The same external_member_id with a different email | The email is not changed. Semis matches by your ID first and only fills an email that is empty. Ask an organisation admin to change a member's email in Semis. |
A new external_member_id for a member Semis has not linked yet and who has no Semis account (for example added manually or by CSV) | Matched by email, linked immediately: your ID is stored on that member. Returns 200, created: false. |
A new external_member_id for a member who already has a Semis account | Matched by email but held for review (409, pending_review). After an Owner, Co-Owner or Admin approves it in Integrations → Sync activity, your ID is stored and later syncs match it directly. |
A different external_member_id for a member already linked to another ID | Held for review (409, pending_review), so one person is never silently re-pointed to a different record. Their existing ID is kept until it is approved. |
Once a member is linked, keep using that ID for every sync, eligibility check and
attendance call. Use GET /unlinked-members/ to find members
you still need to link.
Step 6: configure and test eligibility (optional)
Eligibility needs a separate mapping from your event ID to required Semis courses. Ask Semis platform staff to configure Django admin → Partner integrations → Integration Course Requirements:
- Choose the correct organisation.
- Enter your external event ID, for example
evt_1024. - Select the required Semis course in that organisation.
- Optionally set
valid_within_months; unset means no expiry. - Keep the rule active. Add another rule for each additional required course.
The member-sync request does not create these rules. The partner API currently has no endpoint for configuring them. Members must complete the required learning in Semis; identity mapping alone does not satisfy a requirement.
curl -sS -G -w '\nHTTP_STATUS=%{http_code}\n' \
"$SEMIS_API_BASE/eligibility/" \
-H "Authorization: Bearer $SEMIS_API_KEY" \
--data-urlencode "external_member_id=emp_5567" \
--data-urlencode "external_event_id=evt_1024"
For valid input, both eligible and ineligible decisions return 200. Inspect the body, not just the HTTP status. Example with a known member and incomplete course:
{
"eligible": false,
"member_found": true,
"external_member_id": "emp_5567",
"external_event_id": "evt_1024",
"requirements": [
{
"course_id": "a7012c33-1234-4567-89ab-0123456789ab",
"course_title": "Workplace Safety",
"completed": false,
"completed_at": null,
"valid_within_months": null,
"within_window": true,
"met": false
}
],
"reason": "One or more course requirements are not met."
}
within_window: true without an expiry does not mean completed; use met and the
overall decision. For a missing member with configured rules, member_found and
eligible are false, and requirement entries contain only course_id,
course_title, and completed.
No configured requirement means eligible
Current behaviour returns eligible: true and an empty
requirements list when no active rule matches the event ID, even if
member_found is false. An event-ID typo can therefore look like a positive
decision. For a prerequisite-controlled event, require a known member and the expected
non-empty requirements before permitting attendance. Test both incomplete and
completed courses, not just a successful HTTP response.
Your application enforces the decision. This endpoint checks course-completion requirements, not general membership access/status. It does not automatically disable check-in in your application.
Step 7: report attendance (optional, writes data)
Requires attendance:write. Apply your admission policy first: this endpoint does
not recheck eligibility, mark courses complete, or enrol members.
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
-X POST "$SEMIS_API_BASE/attendance/" \
-H "Authorization: Bearer $SEMIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_event_id": "evt_1024",
"external_member_id": "emp_5567",
"status": "present",
"occurred_at": "2026-09-29T09:05:00Z"
}'
Replace the sample timestamp with the actual occurrence time.
{
"attendance_id": "b8123d44-1234-4567-89ab-0123456789ab",
"external_event_id": "evt_1024",
"external_member_id": "emp_5567",
"status": "present",
"member_found": true,
"created": true
}
Expect 201 for a new record. The same organisation/event/member combination
returns 200, created: false, and updates the existing record rather than
appending a duplicate. Resending can overwrite previous attendance values: retry
with the intended complete payload.
An unknown member can still produce an attendance record with member_found: false.
Resolve the mapping and resend the attendance to link it.
Endpoint reference
Sync a member
/api/v1/partner/members/scope: members:write| Field | Type | Required | Notes |
|---|---|---|---|
external_member_id | string | Yes | Stable ID in your system, maximum 255 characters |
email | string | For new members | Also used to match existing membership records |
first_name | string | No | |
last_name | string | No | |
employee_id | string | No | Separate HR identifier, maximum 50 characters; must pass validation |
department | string | No | Growth department name. Matched to an existing department ignoring case and repeated spaces; created only if nothing matches. See Departments |
department_id | UUID | No | Exact department ID from GET /departments/. Takes precedence over department and never creates a department |
job_title | string | No |
Use ?dry_run=true or a dry_run: true body flag to preview. Inspect any warnings
on a successful sync.
Names keep the convention your members already use
When the member already has a name in Semis, first_name and last_name are converted
to that name's convention before they are saved, so a sync never flips what the member
or an admin set:
- Same name, different convention (
JOHNvsJohn): the existing spelling is kept. - Different name (
Jonathan): it is still applied, converted to the existing convention (Title Case, UPPER or lower). Mixed-case names such asMcDonaldare not altered when the existing name is Title Case. - A new member is created with the names exactly as sent.
This applies to single and batch syncs. Converted fields are recorded in the
organisation activity log entry for the sync (names_converted_to_member_convention).
Departments
/api/v1/partner/departments/scope: members:readRead the departments Semis already has before you sync, and send their IDs so your departments map onto Semis departments instead of creating look-alikes:
{
"departments": [
{
"department_id": "5b0f6a52-3c1e-4f0e-9f57-0d6f1c2a7e11",
"name": "Engineering",
"description": null,
"member_count": 42
}
],
"total": 1,
"allow_multiple_departments": false,
"plan_name": "PROFESSIONAL",
"max_departments": 10,
"remaining_departments": 9,
"can_create_departments": true
}
department_idis an exact match. An unknown ID is skipped with awarningsentry; nothing is created.- A
departmentname is matched ignoring case and repeated spaces (human resourcesreusesHuman Resources). Only when nothing matches is a new department created. - Plan limits apply. A department created by a sync counts against the plan's
department limit (
max_departments,0means unlimited). Oncecan_create_departmentsisfalse, the member is still synced, the department is skipped, andwarningsexplains why. Members are likewise limited by licensed seats: with no free seat a new member is refused (single sync 402, batch statusno_seats). - When
allow_multiple_departmentsisfalse, a member holds one department and a sync replaces it; whentrue, the department is added to the member's others. - Departments created by a sync appear in the activity log as
department.created.
Sync many members
/api/v1/partner/members/batch/scope: members:writeUse a non-empty array of up to 200 members:
{
"members": [
{ "external_member_id": "emp_1", "email": "one@example.com", "first_name": "One" },
{ "external_member_id": "emp_2", "email": "two@example.com", "first_name": "Two" }
]
}
The payload is validated before processing. Once processing starts, member syncs are independent: a service-level failure or blocked member does not roll back the others. Inspect every result; 200 does not mean all members synced.
{
"dry_run": false,
"total": 2,
"created": 1,
"updated": 1,
"blocked": 0,
"failed": 0,
"results": [
{ "index": 0, "external_member_id": "emp_1", "status": "created" },
{ "index": 1, "external_member_id": "emp_2", "status": "updated" }
]
}
Other per-member statuses are pending_review, no_seats and error. Resolve reviews and
retry only appropriate failed items. Batch dry runs use ?dry_run=true; their
created/updated counts are projections, not applied changes. Inspect each preview's
would_block and changed_fields.
Read a member
/api/v1/partner/members/{external_member_id}/scope: members:readReturns the mapped, non-deleted member in the key's organisation, or 404.
departments lists department names and department_details the matching
department_id and name pairs.
member_status is the member's last recorded status in Semis:
member_status | Meaning |
|---|---|
not_invited | Added but no invitation has been sent |
invited | Invitation sent and not yet accepted. The invitation may have lapsed; this endpoint does not recalculate it |
accepted | Invitation accepted, account not yet active |
active | Member can use Semis |
inactive | Member is no longer active |
deactivated | Member deactivated their own account in this organisation |
banned | Member is banned from this organisation |
The same member_status field is also returned by POST /api/v1/partner/members/ (single sync).
department_id values you send are references to existing departments only. They are
never created or changed by the partner API. An unknown department_id is skipped with a
warnings entry, and the member is not assigned to a department.
Members not yet linked
/api/v1/partner/unlinked-members/scope: members:readLists the organisation's members who have no external_member_id yet, so you can see
whose accounts are not linked to your attendance app. Attendance for an unlinked member
is stored with member_found: false and cannot be matched to them. Link a member by
syncing them (matched by email) with your own ID. See
Syncing the same member again.
| Query parameter | Notes |
|---|---|
search | 3–255 characters; matches email, first or last name, or employee ID |
status | not_invited, invited, accepted, active, inactive, deactivated or banned |
role | OWNER, CO_OWNER, ADMIN, MODERATOR or MEMBER |
department_id | A department_id from GET /departments/ |
cursor | Opaque cursor returned as next_cursor. Omit it for the first page; do not construct or alter it. Cursors expire after 24 hours. |
page_size | Default 50, maximum 200 |
{
"results": [
{
"membership_id": "0b6f5f7a-1c2d-4e5f-8a9b-0c1d2e3f4a5b",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Musa",
"employee_id": "E77",
"role": "MEMBER",
"member_status": "active",
"has_account": true,
"departments": [
{ "department_id": "5b0f6a52-3c1e-4f0e-9f57-0d6f1c2a7e11", "name": "Engineering" }
]
}
],
"page_size": 50,
"has_next": false,
"next_cursor": null
}
Results are ordered by email and then membership ID. Pass next_cursor to retrieve the
next page; it is deliberately opaque so the list remains fast for very large organisations.
Removed members are never listed. An invalid filter, cursor, or page size returns 400
with a detail message. has_account: true means the person
has signed up to Semis, so their first link by email is held for review.
Owners and admins see the same list under Integrations → Members not yet linked.
Check eligibility
/api/v1/partner/eligibility/scope: completions:readBoth external_member_id and external_event_id query parameters are required.
See step 6 for prerequisite configuration, decision fields, and no-rule behaviour.
Record attendance
/api/v1/partner/attendance/scope: attendance:write| Field | Type | Required | Notes |
|---|---|---|---|
external_event_id | string | Yes | Event ID in your system, maximum 255 characters |
external_member_id | string | Yes | Member ID in your system, maximum 255 characters |
status | string | No | present (default), absent, late, or excused |
occurred_at | ISO 8601 string | No | Actual attendance time |
raw_payload | JSON | No | Extra reference data; send only what is necessary and authorised |
How to see whether requests reached Semis
Organisation owners and admins
In Organisation Settings → Integrations, fully reload the page after testing:
- Last used tracks successful key authentication and organisation/plan checks, not successful member sync. It normally updates even if the later response is a member-not-found 404, validation error, missing-scope 403, or rate limit.
- Missing/invalid-key attempts do not update Last used. Timestamp tracking is best-effort; a tracking failure is logged without blocking the request.
- Last used is recorded at most once per minute per key. A call made within a minute of the displayed time may not change it.
- The key list does not poll for partner traffic, so an already-open page can be stale.
- Sync activity shows member-sync history/review, not every API call. A read, eligibility check, or dry run does not create an applied member-sync entry.
- Member sync notification outcomes are stored durably in the database in fixed five-minute windows, rather than using cache-based batches or scheduling a task for each request. Admins receive one summary (created, updated, needing review, failed) per window instead of one notification per member or request. Celery beat retries due windows every 60 seconds; summaries are persisted atomically and idempotently so retries do not create duplicate stored summaries.
- Successful and blocked sync counts are recorded inside the sync transaction. A database failure cannot commit the sync while silently losing its durable batch; the transaction rolls back instead. WebSocket refresh is explicitly best-effort after commit, separate from the durable outcome and summary.
- Summaries typically arrive within six minutes when workers and beat are available. This is not an exact five-minute delivery guarantee. After an outage, pending windows are processed when service resumes, so notifications can be delayed. There is no notification for every API read.
- API usage on each key shows its last 800 calls (succeeded, client errors, rate limited, server errors, busiest endpoints) with the time of the most recent call.
- Sync activity is paged and can be filtered by status, so held syncs beyond the first page can be found and reviewed. Search matches contained text in email, name, or external ID. A nonempty search must contain 3 to 255 characters after trimming surrounding whitespace. Shorter or longer searches show a validation message without changing the current results or sending an invalid search. Clear the search to remove the text filter. Page sizes remain 10, 20 (default), 50, or 100.
- Pending cards show the recorded before data, attempted request, and a field-by-field proposal for inspection before approval or rejection. These are recorded request values, not a guarantee of the current member state; approval validates the request and resolves department names/IDs.
- Reviews are available only in Settings → Integrations, including member history, not in the member table. Bulk selection includes only visible pending rows. Changing organisation, history, filters, search, page, page size, or refreshing the activity clears selection. Bulk review requires an explicit confirmation summary; check both the success count and any per-request failures.
- The authenticated admin bulk-review request sends only
snapshot_ids(a nonempty list of at most 200 snapshot UUIDs) andaction(approveorreject). Member IDs are not accepted. The response preservesaction,succeeded, andfailedentries (snapshot_id,member,error). It also includesrequested, the unique snapshot count, andresults, with eachsnapshot_id, astatusofsucceededorfailed, and optionalmemberanderrorfields. The UI continues to use the preserved summary fields; it does not depend onrequestedorresults. There is nomembers_without_pending_syncfield. - Review and undo refresh affected member-table and department caches as well as sync activity. API-key usage still requires a page reload to see partner traffic.
Rate limits and retries
| Response header | Meaning |
|---|---|
X-RateLimit-Limit | Allowed requests in the reported window |
X-RateLimit-Remaining | Remaining requests in that window |
X-RateLimit-Reset | Seconds until the oldest counted request expires from the minute window; not a Unix timestamp |
Rate-limit headers are included when the limiter has populated its state.
On 429, respect Retry-After and the JSON retry_after value when provided.
Limits are per integration key, not per employee or IP address. Read, write,
and batch minute allowances are separate; writes and batches also share a rolling
24-hour allowance. Applicable quotas are checked and consumed atomically.
| Allowance | Professional | Enterprise |
|---|---|---|
| Reads per rolling minute | 120 | 600 |
| Single-member sync / attendance writes per rolling minute | 60 | 300 |
| Batch requests per rolling minute | 30 | 120 |
| Writes + batches per rolling 24 hours | 20,000 | 200,000 |
If the limiter is unavailable, Semis rejects the call with 503 rather than
allowing an unmetered operation. Respect Retry-After. A response explicitly
reporting limiter unavailability means the partner operation was not run; do
not infer that from an unrelated proxy/server 503.
Use bounded retries and backoff for transient failures. Do not automatically repeat authentication errors, invalid payloads, or pending-review syncs.
Use the batch endpoint for imports instead of sending hundreds of individual requests. Attendance is an upsert, not an append-only check-in history.
Errors and troubleshooting
| Status/result | Interpretation |
|---|---|
400 | Missing/malformed fields; inspect field errors or detail/errors |
401 | Missing/invalid/revoked key, inactive organisation, or failed plan entitlement |
403 | Authenticated key lacks the endpoint's required scope |
JSON 404 with Member not found. | Authenticated lookup, but no matching non-deleted member in that organisation |
409 with pending_review | Wait for an authorised reviewer; preserve the review_id |
Other 409 | A data conflict; inspect the message |
429 | Rate limit reached; wait before retrying |
503 reporting an unavailable limiter | Request blocked before the partner operation; respect Retry-After and retry with backoff |
| HTML page or frontend 404 | Verify the API URL and deployed routing |
5xx | Server/proxy failure; retain timestamp/status and contact Semis if persistent |
Serializer validation errors can be field-keyed rather than having a single detail.
For example:
{
"external_member_id": ["This field is required."]
}
Go-live test checklist
| Test | Expected result |
|---|---|
| Read without a key | 401 |
| Read using a valid key, permitted scope, and dummy ID | JSON 404, not HTML |
Same read using a key without members:read | 403 |
| Dry-run member sync | Preview only; no mapping applied |
| Actual approved sync, then lookup | 200 lookup with the correct person |
| Protected email match | 409 pending review; approve/reject before relying on mapping |
| Incomplete required course | HTTP 200, eligible: false |
| All required courses completed within configured validity | HTTP 200, eligible: true, expected requirements present |
| Unknown member with configured requirements | eligible: false, member_found: false |
| Unknown/unconfigured event ID | Empty requirements; do not treat it as completed prerequisite training |
| First attendance report, then identical retry | 201 then 200; same attendance record |
| Attendance for an unmapped ID | member_found: false; sync and resend to reconcile |
| Refreshed integrations screen / staff request logs | Authentication usage and request outcome visible unless tracking failed |
Need a key, an approved test environment, course-requirement configuration, or help with a failed test? Contact your organisation owner or the Semis team.