LTI Launch Flow

OIDC login initiation, launch callback, and JWKS endpoints used by the LMS during an LTI 1.3 launch.

Use this page to see each request an LMS makes during an LTI 1.3 launch, and what TutorFlow answers.

LTI Launch Endpoints

These endpoints are called by the LMS during an LTI 1.3 launch. They are public endpoints and do not require an API key. The LMS redirects the student's browser to these URLs as part of the OIDC third-party login flow.

For a full walkthrough of the launch process, see the LTI Integration Guide.


GET | POST /v1/platform/lti/login/{registrationId}

A registration's own OIDC login initiation URL. Registration responses return its path as loginInitiationPath. Configure the LMS with this URL once the registration exists: the launch resolves to that registration only, so a registration with the same issuer and client ID in another account cannot block or capture it.

It takes the same parameters as the generic route below and answers the same 302. iss must equal the registration's issuer, and client_id, when sent, its clientId; otherwise the launch is refused.

https://api.tutorflow.io/v1/platform/lti/login/a1b2c3d4-e5f6-7890-abcd-ef1234567890
  ?iss=https://canvas.instructure.com
  &login_hint=user-id-12345
  &target_link_uri=https://api.tutorflow.io/v1/platform/lti/launch
  &client_id=10000000000001

GET | POST /v1/platform/lti/login

The generic OIDC login initiation endpoint. It finds the registration by iss and client_id, and refuses the launch when more than one active registration matches; that error names the per-registration URL above. Use it for the first setup step when the LMS asks for a login URL before it has issued the client ID, then switch to the per-registration URL. The LMS redirects the student's browser here to start the LTI launch. TutorFlow validates the parameters, generates a state and nonce, and redirects the browser back to the LMS authorization endpoint.

Both GET (query parameters) and POST (form-encoded body) are supported, as required by the LTI 1.3 specification. Moodle POSTs the login initiation by default.

Query Parameters

ParameterTypeRequiredDescription
issstringYesIssuer identifier of the LMS (must match a registered issuer)
login_hintstringYesOpaque value from the LMS identifying the user
target_link_uristringYesThe URL the LMS wants to launch (TutorFlow's launch URL)
lti_message_hintstringNoOpaque value from the LMS passed through to the authorization request
client_idstringNoThe client ID of the tool registration. On the generic route, needed when the same issuer has more than one registration.

Example

The LMS constructs a URL like this and redirects the student's browser to it:

https://api.tutorflow.io/v1/platform/lti/login
  ?iss=https://canvas.instructure.com
  &login_hint=user-id-12345
  &target_link_uri=https://api.tutorflow.io/v1/platform/lti/launch
  &lti_message_hint=assignment-context-hint
  &client_id=10000000000001

Response

This endpoint does not return a JSON body. It responds with an HTTP 302 redirect to the LMS authorization endpoint. The redirect URL includes the following query parameters:

ParameterDescription
scopeAlways openid
response_typeAlways id_token
response_modeAlways form_post
client_idThe registered client ID
redirect_uriTutorFlow's launch URL
stateCSRF protection token (stored server-side)
nonceReplay protection token
login_hintPassed through from the original request
lti_message_hintPassed through from the original request (if provided)
promptAlways none (SSO, no re-authentication)

Error Responses

StatusCause
401No active LTI registration found for the given iss and client_id, including missing or mismatched iss
401More than one active registration matches iss and client_id. Configure the LMS with the per-registration URL

POST /v1/platform/lti/launch

Launch callback endpoint. After the LMS authenticates the user, it POSTs a form to this URL containing the signed id_token (JWT) and the state value. TutorFlow validates the JWT signature, checks the state and nonce, extracts LTI claims, and creates a launch session.

Request Body (form-encoded)

FieldTypeRequiredDescription
id_tokenstringYesThe signed JWT from the LMS containing LTI launch claims
statestringYesThe CSRF state token generated during the login step

Content Type

Content-Type: application/x-www-form-urlencoded

Example

The LMS auto-submits a form to this endpoint:

HTML
<form method="POST" action="https://api.tutorflow.io/v1/platform/lti/launch">
  <input type="hidden" name="id_token" value="eyJhbGciOiJSUzI1NiIs..." />
  <input type="hidden" name="state" value="csrf-state-token" />
</form>

JWT Claims

The id_token JWT contains standard LTI 1.3 claims:

ClaimDescription
issLMS issuer
subUser identifier in the LMS
audClient ID (must match the registration)
nonceMust match the nonce from the login step
https://purl.imsglobal.org/spec/lti/claim/message_typeLtiResourceLinkRequest or LtiDeepLinkingRequest
https://purl.imsglobal.org/spec/lti/claim/versionAlways 1.3.0
https://purl.imsglobal.org/spec/lti/claim/rolesArray of LTI role URIs
https://purl.imsglobal.org/spec/lti/claim/resource_linkResource link details (id, title)
https://purl.imsglobal.org/spec/lti-ags/claim/endpointAGS endpoint for grade passback (if available)

Response

On success, TutorFlow creates an LTI launch session and redirects the student's browser to the frontend launch page:

{frontend}/platform/lti/launch?sessionId={sessionId}

The response is an HTTP 302 redirect. The launch page resolves the session via GET /v1/platform/lti/sessions/{sessionId}/context and either opens the target content (resource link launches) or renders the content picker (deep linking launches).

Validation Steps

TutorFlow performs the following validation on every launch:

  1. State check: The state parameter must match a valid, unexpired login session (single-use).
  2. JWT signature: The id_token is verified against the LMS JWKS (fetched from the registered jwksEndpoint).
  3. Issuer check: The iss claim must match the issuer in the registration.
  4. Audience check: The aud claim must include the registered clientId; multi-audience tokens must carry a matching azp.
  5. Nonce check: The nonce must match the value generated during the login step.
  6. Expiry check: The token must not be expired (exp claim).
  7. LTI version: The LTI version claim must be 1.3.0.
  8. Required claims: deployment_id is mandatory; resource_link.id is required for LtiResourceLinkRequest; a deep_link_return_url is required for LtiDeepLinkingRequest. Unknown message types are rejected.
  9. Claim capture: Message type, deployment, resource link, AGS, NRPS, and deep linking claims are stored on the launch session when present.

Error Responses

StatusCause
401Missing, invalid, expired, or already-processed state token
401JWT signature verification failed
401Issuer, audience/azp, or nonce mismatch
401Token expired
401Unsupported LTI version or message type
401Missing deployment_id or resource_link.id claim

GET /v1/platform/lti/sessions/{sessionId}/context

Resolves a completed launch session for the frontend launch page: message type, target link URI, roles, and deep linking settings. Sessions are short-lived (the launch TTL, 10 minutes by default) and return 401 once expired or consumed.

Response

JSON
{
  "sessionId": "session-uuid",
  "status": "COMPLETED",
  "messageType": "LtiDeepLinkingRequest",
  "targetLinkUri": null,
  "roles": ["http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor"],
  "launchUserName": "Jane Instructor",
  "launchUserEmail": "jane@school.edu",
  "workspaceName": "Acme Academy",
  "hasAgsLineItem": false,
  "deepLinking": {
    "acceptTypes": ["ltiResourceLink"],
    "acceptMultiple": true,
    "acceptLineItem": true,
    "autoCreate": false,
    "title": null
  },
  "expiresAt": "2026-07-16T12:10:00.000Z"
}

GET /v1/platform/lti/sessions/{sessionId}/content

Lists the content available to the deep linking picker. Only valid for deep-linking launch sessions. For a registration created with an API key, it lists the content the workspace made on the /v1/platform content routes before they were removed on 2026-10-16, not content made through the Content API. url is the item's old Platform link; see Old share links for where those links lead now. See also Deep Linking.

Response

JSON
{
  "items": [
    {
      "id": "resource-uuid",
      "type": "test",
      "title": "Algebra Quiz",
      "url": "https://tutorflow.io/en/platform/tests/take/abc123",
      "scoreMaximum": 100,
      "createdAt": "2026-07-01T00:00:00.000Z"
    }
  ]
}

POST /v1/platform/lti/sessions/{sessionId}/deep-link

Builds the signed LtiDeepLinkingResponse for the resources the LMS user picked. The content items are constructed server-side from the referenced workspace resources (the client only sends resource references), the launch session is consumed (one-time use), and gradeable resources (tests) automatically include an AGS lineItem when the LMS offered accept_lineitem.

Request Body

FieldTypeRequiredDescription
itemsarrayYesSelected resources. An empty array returns a no-selection (cancel) response.
items[].resourceTypestringYesOne of course, module, slide, video, test
items[].resourceIdstring (uuid)YesId of the resource in the workspace

Example Request

bash
curl -X POST https://api.tutorflow.io/v1/platform/lti/sessions/dl-session-uuid/deep-link \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "resourceType": "test", "resourceId": "resource-uuid" }
    ]
  }'

Response

JSON
{
  "jwt": "eyJhbGciOiJSUzI1NiIs...",
  "returnUrl": "https://lms.example.com/courses/1/deep_linking_response"
}

The frontend auto-submits jwt as the JWT form field via POST to returnUrl, completing the deep linking flow in the LMS.

Error Responses

StatusCause
400Session is not a deep linking request, selection violates accept_multiple, or the platform does not accept ltiResourceLink
401Session expired or already consumed
404A referenced resource does not exist in the workspace

GET /v1/platform/lti/.well-known/jwks.json

Returns TutorFlow's public JSON Web Key Set (JWKS). The LMS uses this endpoint to retrieve the public keys needed to verify JWTs signed by TutorFlow (used during deep linking responses and other tool-initiated messages).

Authentication

None. This is a public endpoint.

Example Request

bash
curl https://api.tutorflow.io/v1/platform/lti/.well-known/jwks.json

Response

JSON
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "tutorflow-lti-key-1",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQSuu...",
      "e": "AQAB"
    }
  ]
}

Response Fields

FieldTypeDescription
keysarrayArray of JWK objects
keys[].ktystringKey type (always RSA)
keys[].kidstringKey ID used to match the kid header in JWTs
keys[].usestringKey usage (always sig for signature verification)
keys[].algstringAlgorithm (always RS256)
keys[].nstringRSA modulus (Base64url-encoded)
keys[].estringRSA exponent (Base64url-encoded)

Caching

This endpoint supports HTTP caching. The response includes Cache-Control headers. Keys may be rotated periodically. LMS platforms should re-fetch the JWKS if JWT verification fails with a kid mismatch.