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

# Learning Activities API

> Check whether a learner completed a learning item, or sync learner progress to your own systems, through the Juno API.

Use the learning activities endpoint to answer "did this learner complete this learning item?" in one call, or to keep your own system (an HRIS, ServiceNow, a data warehouse) in step with learner progress. It returns one row per learner per learning item: assignment, progress, completion, and score, as they are now.

***

## Before you start

* Create a personal access token with the **Read learning activities** scope (`learning_activities:read`). See [Authentication](/integrations/api-overview#authentication).
* The token works only while the Admin who created it is still an Admin. If that person is no longer an Admin, requests return `403`: create a new token.

***

## Check if a learner completed a learning item

Send the learner's email and the learning item's ID:

```bash theme={null}
curl -X POST "https://api.the-juno.com/api/v1/public-api/learning-activities/.search-cursor" \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": { "user_email": "dana.levi@acme.com", "unit_id": "64f0c0ffee0000000000abcd" },
    "select": { "status": 1, "completed_at": 1 }
  }'
```

```json theme={null}
{
  "data": [
    {
      "activity_id": "9b2f1c5e8a7d4e3f9c0b1a2d3e4f5a6b",
      "status": "completed",
      "completed_at": "2026-10-19T09:41:07.000Z",
      "last_activity_at": "2026-10-19T09:41:07.000Z"
    }
  ],
  "cursor": "eyJ0aW1lc3RhbXAiOi...",
  "dataAsOf": "2026-10-19T09:52:30.912Z"
}
```

**The learner completed the item when `completed_at` is set.** That includes completions an Admin recorded with **Mark as completed** for a learner who already had the item (assigned or started). Email matching ignores case. An email that doesn't belong to a learner in your organization returns an empty `data` list.

A learner can have more than one row for the same learning item, for example one per live session they attended, or one per place they took it (on its own and inside a journey); `consumption_context_id` tells those apart. Treat the item as completed when any row has `completed_at` set, and count learners by distinct `user_id`, not by rows.

> [!WARNING]
>
> Each row is the learner's **current** state for the item, and date filters match current values. If a completion is later reset, reopened or retaken, or the assignment is removed, the earlier completion no longer appears. Use this endpoint for the latest status, not for history such as "how many learners completed this on a past date".

### Status values

| `status` | Used for |
| - | - |
| `not_started`, `in_progress`, `completed` | Standard learning, SCORM, journeys, videos, articles, and external content |
| `in_review`, `passed`, `failed` | Quizzes. `in_review` while answers wait for grading |
| `attended`, `not_attended` | Live events |

> [!NOTE]
>
> SCORM learning ends as `completed`, not `passed`. A quiz ends as `passed` or `failed`, and gets `completed_at` only when it's passed or an Admin marks it as completed.

***

## Request body

All fields are optional. A request with an empty body returns the first page of every learning activity in your organization.

| Field | Description |
| - | - |
| `filters` | Narrow the rows. See [Filters](#filters). |
| `select` | The fields to return, for example `{ "status": 1, "completed_at": 1 }`. `activity_id` and `last_activity_at` are always included. Leave it out to get every field. |
| `limit` | Rows per page: 1 to 1,000. Default 50. |
| `cursor` | The `cursor` from the previous response, to get the next page. |

> [!TIP]
>
> Select only the fields you use. Smaller responses come back faster.

### Filters

Each filter takes a single value or `{ "$in": [ ... ] }` with up to 500 values. Combine filters to narrow further.

Date filters take `from` (on or after) and `to` (on or before), the same as the xAPI search endpoint. Values are ISO 8601: a date-time with `Z` or an offset (`2026-10-01T09:30:00Z`, `2026-10-01T12:30:00+03:00`), or a date (`2026-10-01`), which means midnight UTC at the start of that day. So `"to": "2026-10-31"` stops at the first moment of October 31: to include the whole day, use `"to": "2026-10-31T23:59:59.999999Z"`.

> [!TIP]
>
> For back-to-back windows, use `$gte` (on or after) and `$lt` (before) instead: `$lt` excludes its own value, so no row falls into two windows. Use one style per date filter: mixing `from`/`to` with `$gte`/`$lt` returns `400`.

| Filter | Example |
| - | - |
| `user_email` | `"user_email": "dana.levi@acme.com"` |
| `user_id` | `"user_id": { "$in": ["64f0…", "64f1…"] }` |
| `unit_id` | `"unit_id": "64f0c0ffee0000000000abcd"` |
| `unit_type` | `"unit_type": { "$in": ["scorm", "quiz"] }` |
| `status` | `"status": "completed"` |
| `assigned_at`, `started_at`, `completed_at`, `due_date`, `last_activity_at` | `"completed_at": { "from": "2026-10-01", "to": "2026-10-31T23:59:59.999999Z" }` |

Any other filter name or operator returns `400`.

***

## Fields returned

| Field | Description |
| - | - |
| `activity_id` | Unique ID of this row. Use it to update rows you already stored. |
| `user_id`, `user_email`, `user_name` | The learner |
| `unit_id`, `unit_title`, `unit_type` | The learning item, for example `scorm`, `quiz`, `learning`, `path` (a journey), `event` |
| `consumption_context_id` | Where the learner took the item: `0` on its own, otherwise the ID of the unit it was taken inside, such as a journey |
| `status` | Progress. See [Status values](#status-values). |
| `assigned_at` | When the item was assigned. Empty for live events. |
| `started_at` | When the learner started. For live events, the session start. |
| `completed_at` | When the learner completed it |
| `due_date` | The due date of the assignment. Empty for live events. |
| `last_activity_at` | The learner's latest progress on this item. Results are sorted by it. |
| `score`, `graded_at` | Quiz and scored SCORM results; empty for other learning |

Dates are ISO 8601 in UTC. A field with no value is `null`.

***

## Page through results

Results are sorted by `last_activity_at`, oldest first. Rows with no activity yet come first. Every response returns a `cursor`. Send it back to get the next page, and stop when a page has fewer rows than your `limit`:

```json theme={null}
{ "limit": 1000, "cursor": "eyJ0aW1lc3RhbXAiOi..." }
```

Each row appears once while you page. If a learner makes progress while you page, their row can appear again later with the newer values: update rows by `activity_id`. A page with no rows returns the cursor you sent.

Use the cursor to page through one run. Don't save it to resume a later run: activity can reach the API after rows with a later `last_activity_at`, and a saved cursor would skip it. For later runs, follow [Keep your system in sync](#keep-your-system-in-sync).

## Keep your system in sync

To copy learner progress into another system on a schedule:

**1. Load everything once**

Page through with no filters and store every row by `activity_id`.

**2. Remember where you stopped**

Keep the highest `last_activity_at` you received.

**3. Fetch what changed**

On each run, filter `"last_activity_at": { "from": "<that value minus 1 hour>" }`, page through, and update rows by `activity_id`. The hour of overlap catches changes that reach the API a little late.

`last_activity_at` moves forward when a learner's progress on the item changes: an assignment, a start, progress, a completion, or a new due date. A change to the learner's profile alone, such as a new name or email, doesn't move it. When an assignment is removed, its row stops appearing in results.

***

## Data freshness

Learning activity usually reaches the API within about 15 minutes of happening in Juno, completions recorded with **Mark as completed** included (for learners who already had the item). Every response includes `dataAsOf`, the time the data was last updated. If you need an answer right after a learner finishes, allow for this delay, for example by checking again later.

***

## Limits and errors

Each token can make 60 requests per minute. Above that, requests return `429` until the minute resets; the `Retry-After` header says when to try again.

| Code | Meaning |
| - | - |
| `400` | Invalid body: an unknown field, filter, operator, or cursor |
| `401` | Missing or invalid token |
| `403` | The token lacks `learning_activities:read`, or its creator is no longer an Admin |
| `429` | More than 60 requests in a minute with this token |


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