Skip to main content

Handling Service Requests

Service requests run in the opposite direction from everything else in Connect: LTIAAS calls you.

When a tool asks for a course roster or writes a grade, it makes a standard LTI® service call to LTIAAS. LTIAAS authenticates it, checks the tool's permissions, and then forwards the question to your Service URL, because only your LMS knows the answer. You reply, and LTIAAS translates your reply back into the protocol.

Every service request — all nine types — arrives at that one URL. You branch on the type claim.

The Envelope

Every request is a POST with a single field containing a signed JWT:

{
"payload": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Verify it with your consumer public key, then read type and parameters:

{
"type": "MEMBERSHIPS_GET",
"parameters": {
"context": "2022CSEa5e6c431b91",
"clientId": "qR8E0iHqSdR30DdfQAbcaBGjKT65"
}
}

context is the identifier you supplied when you started the launch. clientId identifies the tool making the request — useful for logging, and for refusing tools that should not be asking.

caution

DEEP_LINKING_RESPONSE arrives form-urlencoded; the other eight arrive as JSON. It is delivered by a form in the user's browser rather than a server-to-server call. Parse both content types on this route.

The Skeleton

const jwt = require('jsonwebtoken')

app.post('/lti-services',
express.json(),
express.urlencoded({ extended: true }), // for DEEP_LINKING_RESPONSE
async (req, res) => {
let decoded
try {
decoded = jwt.verify(req.body.payload, LTIAAS_PUBLIC_KEY)
} catch (err) {
return res.status(401).json({ error: 'Invalid payload' })
}

const { context, clientId } = decoded.parameters

switch (decoded.type) {
case 'MEMBERSHIPS_GET': return handleMemberships(decoded, res)
case 'DEEP_LINKING_RESPONSE': return handleDeepLinking(decoded, res)
case 'LINEITEMS_GET': return handleLineItemsList(decoded, res)
case 'LINEITEMS_POST': return handleLineItemCreate(decoded, res)
case 'LINEITEM_GET': return handleLineItemGet(decoded, res)
case 'LINEITEM_PUT': return handleLineItemUpdate(decoded, res)
case 'LINEITEM_DELETE': return handleLineItemDelete(decoded, res)
case 'SCORE_POST': return handleScore(decoded, res)
case 'RESULTS_GET': return handleResults(decoded, res)
default: return res.status(400).json({ error: 'Unknown type' })
}
}
)

The Response Contract

This is the part worth reading twice, because success and failure are treated very differently.

On success, return 200 with JSON in the documented shape. LTIAAS validates the schema before passing it to the tool, so a wrong shape fails the tool's request even though you returned a 200.

On failure, return any 4xx or 5xx with any body. Non-2xx responses are converted to JSON and forwarded to the tool untouched — no schema validation. This is how you say "that line item does not exist" or "that tool may not see this course": return a 404 or a 403 and the tool receives it.

// A grade line the tool asked for but which we do not have
if (!lineItem) {
return res.status(404).json({ error: 'Line item not found' })
}
tip

Prefer a truthful 404 over an empty 200. Tools handle "not found" correctly far more often than they handle "found, but empty".

The Nine Request Types

TypeTool is askingGuide
MEMBERSHIPS_GETFor the course rosterMemberships
DEEP_LINKING_RESPONSETo hand back selected contentDeep linking
LINEITEMS_GETFor the list of grade linesLine items
LINEITEMS_POSTTo create a grade lineLine items
LINEITEM_GETFor one grade lineLine items
LINEITEM_PUTTo update a grade lineLine items
LINEITEM_DELETETo delete a grade lineLine items
SCORE_POSTTo record a gradeScores and results
RESULTS_GETFor recorded gradesScores and results

You do not have to support all of them. Implement the ones matching the permissions you grant your tools, and return a 4xx for the rest — a tool without LINEITEMS_READ_WRITE will never send you a LINEITEMS_POST anyway.

When These Arrive

Service requests are not part of a launch. A tool might read the roster the moment a teacher opens it, or post grades in a nightly batch hours after everyone has gone home. Your Service URL has to work without a user session — authentication is the JWT signature, nothing else.

caution

Do not put session middleware, CSRF protection or a login redirect in front of this route. There is no browser and no cookie; there is a server-to-server call carrying a signed token.

Section Summary

All trademarks, logos, and service marks displayed on this website are the property of their respective owners. LTIAAS is a trademark of GatherAct, LLC, doing business as LTIAAS. Learning Tools Interoperability (LTI)® and LTI® are trademarks of 1EdTech Consortium, Inc. LTIAAS is not affiliated with, endorsed or sponsored by 1EdTech Consortium, Inc. or by any other owners of third-party trademarks used on this website. LTIAAS is not responsible for the content, quality, or accuracy of any websites linked to or from this website that are not owned by LTIAAS. If you have any questions or concerns about the use of any trademarks or content on this website, please contact us.