> For the complete documentation index, see [llms.txt](https://developerdocs.instructure.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developerdocs.instructure.com/services/journey/api-program-enrollment.md).

# Program Enrollment API

This guide explains how to programmatically enroll Canvas users into Journey Programs using the GraphQL API.

## Prerequisites

* A Canvas API token with admin access (`read_as_admin` permission on the target account)
* The Canvas instance URL for your organization
* The Journey GraphQL endpoint URL for your environment
* `jq` (for shell examples)

## Authentication

Journey uses Canvas-issued JWTs for API authentication. Every request requires a Bearer token obtained from Canvas.

### Obtain a Journey JWT

```bash
TOKEN=$(curl -s -X POST "https://<canvas-host>/api/v1/jwts?audience=Instructure&workflows[]=journey" \
  -H "Authorization: Bearer <canvas-api-token>" \
  | jq -r '.token' | base64 -d)
```

Use `$TOKEN` as the `Authorization: Bearer` header on all GraphQL requests. Tokens are short-lived — obtain a fresh one at the start of each batch job.

***

## GraphQL Endpoint

```
POST https://<journey-host>/graphql
Content-Type: application/json
Authorization: Bearer <journey-jwt>
```

***

## Enrolling Users in a Program

### Mutation: `batchEnrollUserToProgram`

{% openapi src="/files/cMs6o16T3opFRrZHGIR8" path="/graphql" method="post" %}
[program-enrollment.yml](https://3935729257-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FB0qnrcLHZo7GMoCVWI3W%2Fuploads%2Fgit-blob-8552564321a0168ae76f31527969380d7f24978e%2Fprogram-enrollment.yml?alt=media)
{% endopenapi %}

Enrolls a list of Canvas users into a single Program.

```graphql
mutation BatchEnrollUsers($programId: String!, $userIds: [String!]!, $accountId: String!) {
  batchEnrollUserToProgram(programId: $programId, userIds: $userIds, accountId: $accountId) {
    id
    enrollee
    createdAt
  }
}
```

#### Arguments

| Argument    | Type                       | Description                              |
| ----------- | -------------------------- | ---------------------------------------- |
| `programId` | `String` (UUID)            | The Journey Program to enroll users into |
| `userIds`   | `[String!]` (Canvas UUIDs) | Canvas user UUIDs to enroll              |
| `accountId` | `String`                   | Canvas account ID (usually `"1"`)        |

#### Response fields

| Field       | Type            | Description                                                                                                                       |
| ----------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `id`        | `String` (UUID) | Journey enrollment record ID                                                                                                      |
| `enrollee`  | `String` (UUID) | Internal Journey user ID (v4 UUID) of the enrolled user — the Journey-issued ID, **not** the Canvas user UUID passed in `userIds` |
| `createdAt` | `DateTime`      | Timestamp when the enrollment was created                                                                                         |

#### Re-enrollment behavior

If a user is already enrolled in the Program, the existing enrollment is returned with no error. It is safe to include already-enrolled users in a batch.

***

## Example Request

```bash
PROGRAM_ID="<program-uuid>"
ACCOUNT_ID="1"
USER_IDS='["uuid-1","uuid-2","uuid-3"]'

curl -s -X POST "https://<journey-host>/graphql" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$(jq -n \
    --arg programId "$PROGRAM_ID" \
    --arg accountId "$ACCOUNT_ID" \
    --argjson userIds "$USER_IDS" \
    '{
      query: "mutation BatchEnrollUsers($programId: String!, $userIds: [String!]!, $accountId: String!) { batchEnrollUserToProgram(programId: $programId, userIds: $userIds, accountId: $accountId) { id enrollee createdAt } }",
      variables: { programId: $programId, userIds: $userIds, accountId: $accountId }
    }')"
```

***

## Batching Large Enrollments

There is no server-enforced limit on `userIds` length, but the cost scales with **users × program requirements**: each user triggers roughly `5 + (2 × R)` database operations, where R is the number of requirements in the program.

Recommended batch sizes:

| Program requirements | Recommended batch size |
| -------------------- | ---------------------- |
| 1–5                  | 200                    |
| 6–15                 | 100                    |
| 16+                  | 50                     |

Already-enrolled users are safely skipped (idempotent), so retrying a failed batch with the same user list is safe.

```bash
# Pseudocode: split users.txt into batches of 100
split -l 100 users.txt batch_
for file in batch_*; do
  # build USER_IDS from $file and call the mutation
done
```

### Rate limiting

The API enforces per-user rate limits. When exceeded it returns HTTP 429, which Apollo surfaces as a GraphQL error with message `"Too Many Requests"`. The error body includes a `retryAfter` field (seconds) indicating how long to wait.

Recommendations for bulk jobs:

* Sleep **3 seconds** between batch calls as a baseline.
* On a `"Too Many Requests"` error, read the `retryAfter` value from the error extensions and wait that many seconds before retrying.
* Limit retries to **5 attempts** per batch before treating it as a hard failure.

```bash
# Detect rate limit and read retry-after in a shell script
ERR=$(echo "$GQL_RESPONSE" | jq -r '.errors[0].message // empty')
RETRY_AFTER=$(echo "$GQL_RESPONSE" | jq -r '.errors[0].extensions.retryAfter // 10')
if [[ "$ERR" == "Too Many Requests" ]]; then
  sleep "$RETRY_AFTER"
  # retry the batch
fi
```

***

## Finding a Program ID

The `programId` is a UUID visible in the Journey admin UI URL when viewing a Program. To look it up programmatically, use the `programs` GraphQL query, which returns every non-deleted Program in the account when the caller has `read_as_admin`. Match on `name` client-side.

```graphql
query FindPrograms($accountId: String!) {
  programs(accountId: $accountId) {
    id
    name
  }
}
```

> **Note:** `programs` is part of the experimental Programs surface — its shape may change without notice.

***

## Fetching Canvas User UUIDs

Canvas user UUIDs (not numeric IDs) are required. Fetch them from the Canvas Accounts API:

```bash
curl -s "https://<canvas-host>/api/v1/accounts/<account-id>/users?include[]=uuid&per_page=100" \
  -H "Authorization: Bearer <canvas-api-token>" \
  | jq -r '.[].uuid'
```

Paginate with `?page=2`, `?page=3`, etc. until you receive fewer than `per_page` results.

***

## Error Reference

| Scenario                                     | Error                                                                                                                                                                           |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Program UUID does not exist                  | `NotFoundException: Program not found: <id>`                                                                                                                                    |
| Program is not open for enrollment           | `BadRequestException: Program is not open to enroll learners`                                                                                                                   |
| Canvas returns 403 on the permission lookup  | `403 Forbidden`: `"You do not have permission to enroll users to programs."`                                                                                                    |
| Caller's `read_as_admin` permission is false | `401 Unauthorized`: `"You do not have permission to enroll users to programs. Required Canvas permission: read_as_admin. Contact your Canvas administrator to request access."` |
| Invalid or expired JWT                       | `401 Unauthorized`                                                                                                                                                              |
| Too many requests                            | GraphQL error: `"Too Many Requests"` (HTTP 429) — read `retryAfter` from error body and wait                                                                                    |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://developerdocs.instructure.com/services/journey/api-program-enrollment.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
