> ## Documentation Index
> Fetch the complete documentation index at: https://docs.katilyst.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting a training platform

> Award credit when champions complete security training, using Secure Code Warrior and Security Journey as worked examples.

Training platforms hold some of the clearest evidence of champion engagement: courses finished, assessments passed, tournaments entered. This guide connects that activity to Katilyst so it earns recognition automatically.

It uses Secure Code Warrior and Security Journey as the worked examples, and the approach applies to any platform that exposes an API. The two differ in one way that shapes the whole integration, which is where this starts.

## Decide what you are rewarding

Both platforms expose several kinds of activity, and a program does not have to treat them alike. Finishing a single lesson and completing a full learning path represent different amounts of effort, and your Available Actions can reflect that.

**Secure Code Warrior**

| Activity | Endpoint |
| - | - |
| Course and module completions | `/learning/progress` |
| Assessment results | `/assessments`, `/assessments/{id}/attempts` |
| Tournament results | `/tournaments` |
| Overall training progress | `/training/developers-progress`, `/training/developer-leaderboard` |

**Security Journey**

| Activity | Endpoint |
| - | - |
| Individual lesson completions | `/lesson-attempts` |
| Completed learning paths | `/path-enrollments` |
| Assessment results | `/assessments` |
| Champion Passport activities | `/activities` |

Most programs start with one and add others as the program matures. Crediting both a lesson and the path it belongs to is a deliberate choice rather than a mistake — it rewards steady progress and completion separately — but decide it rather than discovering it.

## Does your source support an incremental read?

This is the question that determines how your integration is built, and the answer depends on which endpoint you chose above.

An **incremental read** means the API can return only what changed since you last asked. Where it exists, you pull a handful of records each run. Where it does not, you pull everything every time and sort it out yourself.

> **Yes — incremental**
>
> Pull only what changed, using the platform's date parameter. Volume stays small and the ledger described below is a safety net rather than the mechanism.
>
> **Security Journey `/path-enrollments`** supports `updated_after`.
>
> **Secure Code Warrior assessments and leaderboard** support `startdate` and `enddate`.

> **No — full dump**
>
> Every call returns the complete history. You filter locally before doing anything expensive, and a ledger of what you have already credited becomes the integration rather than a convenience.
>
> **Secure Code Warrior `/learning/progress`** returns all completions for all developers on every call.
>
> **Security Journey `/lesson-attempts`** documents no filtering.

Note that Security Journey lands on both sides depending on what you reward. Crediting completed paths gives you an incremental read; crediting individual lessons does not. Same platform, same customer, two different implementations.

> [!NOTE]
>
> Check the current API reference for your platform before building. Date filtering tends to appear endpoint by endpoint rather than across an API, and parameters get added over time.

## Connecting

**Secure Code Warrior**

**Base URL:** `https://portal-api.securecodewarrior.com/api/v2`

European tenants use `https://portal-api.eu.securecodewarrior.com/api/v2` instead. If you are not sure which applies, your Customer Success Manager can tell you.

**Authentication:** an `X-API-Key` header carrying the token on its own, with no prefix.

```bash theme={null}
curl "https://portal-api.securecodewarrior.com/api/v2/learning/progress" \
  --header "X-API-Key: your_scw_key"
```

Keys come in Report, Admin, and Team varieties, and each endpoint specifies which ones authorise it. A Report key is enough for everything in this guide.

**Pagination:** responses carry a `links` object with `total_pages` and `total_results`. Compare `total_results` against what you actually received rather than assuming one call returned everything — `/learning/progress` serves 2000 records per page, so an organisation larger than that gets silently truncated.

**Security Journey**

**Base URL:** `https://my.securityjourney.com/api/v3/public-reporting`

A few endpoints sit under `/svc/public-reporting` instead. Check the reference for the one you are using.

**Authentication:** an `Authorization` header with a `Bearer` prefix.

```bash theme={null}
curl "https://my.securityjourney.com/api/v3/public-reporting/lesson-attempts" \
  --header "Authorization: Bearer your_sj_token" \
  --header "Content-Type: application/json"
```

A Security Journey Admin creates keys under **Admin → Connections → Reporting API**.

**Pagination:** `path-enrollments` takes `limit` (up to 3000) and `offset`.

## Configure the action in Katilyst

**1. Create an Available Action**

Create an Instance action named for what it recognises — **Secure Code Warrior — Module Completed**, for example. Set the Attribute it changes, the amount, and any conditions.

Create a separate action for each kind of activity you decided to reward. One for lessons and one for completed paths, if that is your model.

> [!TIP]
>
> Use Instance actions here. Training completions are events that happened rather than states that persist. See [Core concepts](https://docs.katilyst.com/core-concepts.md) for the distinction.

**2. Find your project ID**

```bash theme={null}
curl https://api.katilyst.com/v1/projects \
  --header "Authorization: API-Key your_katilyst_key"
```

**3. Find your action ID**

```bash theme={null}
curl https://api.katilyst.com/v1/projects/{projectId}/available-actions \
  --header "Authorization: API-Key your_katilyst_key"
```

Note the `id` for each action you created. You will need one per activity type.

> [!WARNING]
>
> Frequency limits configured on an Available Action do not apply to actions logged through the API. If a developer's completion history is replayed, every record logs. The ledger described below is what prevents that, and it is your responsibility rather than Katilyst's.

## The pattern

Five stages, whatever you build this in.

**1. Pull**

Call the platform on a schedule. Daily is almost always enough — training completions arrive at human pace, and a tighter loop costs more for no benefit.

If your endpoint supports an incremental read, pass a window slightly wider than your schedule interval. Two days against a daily run means a skipped or delayed run still catches its records.

If it does not, you receive everything and move to the next stage.

**2. Filter**

Drop anything outside your window before doing anything else.

**This stage has to come first among the per-record steps.** On a full-dump source it is the difference between examining a handful of records and examining thousands on every run. Filtering after a database lookup or an API call means paying for work you then discard.

Use the completion timestamp the platform returns, and use the same wider-than-the-interval window as above. Overlap is safe because the next stage catches repeats.

**3. Check the ledger**

For each surviving record, ask whether you have credited this person for this item before.

The ledger is a simple key-value store: a Make data store, a Zapier table, a database table, a file. The key identifies one person and one item — an email address and a course identifier, joined.

Katilyst cannot do this for you. It records that an action was logged, not which course produced it, so two completions of different modules are indistinguishable to it. Without a ledger, every overlapping window credits the same work again.

> [!TIP]
>
> Hash the key rather than concatenating raw values. Course identifiers can be long — Secure Code Warrior returns a five-level path that joins to over 150 characters — and a hash gives you a fixed-length key that cannot collide through accidental concatenation.

**4. Log the action**

For records not already in the ledger, call Katilyst.

```json theme={null}
{
  "userEmail": "developer@example.com",
  "availableActionId": "your-action-uuid",
  "dateTime": "2026-01-20T16:24:53.918Z",
  "creator": "Secure Code Warrior sync",
  "details": "Injection Flaws / SQL Injection"
}
```

Pass `dateTime` from the platform's completion timestamp rather than letting Katilyst stamp on receipt. For a nightly job the two differ by hours, and reporting by period depends on the real date.

Put something recognisable in `creator`, and the course name in `details` — that is what appears in the champion's own activity log.

**5. Record it**

Write the key to the ledger, **after** the call succeeds.

The ordering is deliberate. Writing first means a failed call permanently suppresses that credit with nobody noticing. Writing after means a successful call followed by a failed write credits again next run — visible in the champion's log, and correctable.

## Before the first live run: seed the ledger

**On a full-dump source, skipping this step backfills everything.**

The first run sees every completion the platform has ever recorded, finds none of them in an empty ledger, and logs all of them. A developer who has been using the platform for two years collects two years of credit overnight, and the leaderboard stops meaning anything.

Seed first:

1. Run the integration with the Katilyst call disabled and the ledger write enabled.
2. Remove the date filter so everything passes through.
3. Let it complete. The ledger now holds every existing completion, marked as already credited.
4. Re-enable the call and restore the filter.

From then on, only genuinely new completions are credited.

**To backfill deliberately** — crediting the last quarter, say — seed with a filter that excludes that period, so those records stay absent from the ledger and get credited on the first live run.

## Worked example: Secure Code Warrior via Make.com

One implementation of the pattern. The stages map onto any workflow tool; the module names are Make's.

| # | Module | Purpose |
| - | - | - |
| 1 | Schedule — daily | |
| 2 | HTTP → Make a request | Pull. `GET /learning/progress` with the `X-API-Key` connection |
| 3 | Iterator | One bundle per developer: `{{2.data.developers}}` |
| 4 | Iterator | That developer's completions: `{{3.completed}}` |
| 5 | **Filter** | `{{4.time_completed}}` later than `{{addDays(now; -2)}}` |
| 6 | Data store → Get a record | Ledger lookup, on the key below |
| 7 | **Filter** | `{{6.key}}` does not exist |
| 8 | HTTP → Make a request | `POST` to Katilyst |
| 9 | Data store → Add/replace | Ledger write, same key as module 6 |

Modules 6 and 9 share one key expression, which identifies a single person and a single course:

```text theme={null}
{{md5(3.profile.email & "|" & join(4.resource; "/"))}}
```

Four details that are easy to get wrong:

**Scope the inner iterator to the current developer.** `{{3.completed}}` gives you that developer's completions. Referencing the original response with empty brackets — `{{2.data.developers[].completed}}` — collects every completion by everyone, and each developer then gets credited for all of them.

**Put the date filter at module 5, before the data store lookup.** Filters cost nothing in Make; data store operations do. In the wrong position it saves nothing.

**`resource` is an array, not a string.** Secure Code Warrior returns a breadcrumb path. Use `join()` for both the ledger key and the `details` field.

**Handle `409` rather than failing on it.** A `409` means that person is not eligible for the action, which happens routinely when someone outside the champion program completes training. Without an error handler, a few of them halt the run and every later record in that batch is lost. Attach a handler to module 8 that resumes on `409` and let other statuses fail normally.

Set the scenario to process sequentially. Parallel execution lets two bundles both pass the ledger check before either writes.

## Verify

1. **Two developers with multiple completions each.** The only test that catches an incorrectly scoped iterator. Confirm each person is credited for their own work and nobody else's.
2. **Run twice with no new activity.** The second run should log nothing.
3. **Check your operation or task count on a quiet run.** A number close to your developer count means the date filter is working. A number close to your total completion count means it is in the wrong place.
4. **Read an action back.** Confirm `actionDate` matches the completion time rather than when the job ran, and that `details` reads as a course name.

```bash theme={null}
curl "https://api.katilyst.com/v1/projects/{projectId}/users/{userId}/logged-actions" \
  --header "Authorization: API-Key your_katilyst_key"
```

## Troubleshooting

* **Everyone is credited for everyone's completions.** The inner loop is reading the whole response rather than the current person's records. This is invisible in a single-developer test.
* **The same completions are credited on every run.** Either the ledger is never written, or nothing acts on the lookup result. Both halves are needed: a filter that stops already-credited records, and a write that records new ones.
* **The first run credited years of history.** The ledger was not seeded. Credit can be removed in Katilyst, and seeding before the next run prevents a repeat.
* **Consumption is far higher than expected.** The date filter is positioned after an operation that costs something. Move it ahead of the ledger lookup.
* **A `401` from the training platform.** Secure Code Warrior wants `X-API-Key` with a bare token; Security Journey wants `Authorization` with a `Bearer` prefix. They are not interchangeable.
* **A `401` from Katilyst.** The key is invalid, missing, or sent without the `API-Key` prefix on the header value.
* **A `409` from Katilyst.** That person is not eligible for the Available Action. The request was well formed and the program's own rules declined it.
* **Some developers are never credited.** Check the platform's pagination. Compare the total it reports against the number of records you received — a large organisation can exceed one page.

## Related

> **[Core concepts](https://docs.katilyst.com/core-concepts.md)**
>
> Instance versus State actions, guardrails, and how rewards are decided.

> **[Log an action](https://docs.katilyst.com/api-reference/v1/log-an-action.md)**
>
> Full reference for the endpoint this integration calls.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.