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.
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' })
}
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
| Type | Tool is asking | Guide |
|---|---|---|
MEMBERSHIPS_GET | For the course roster | Memberships |
DEEP_LINKING_RESPONSE | To hand back selected content | Deep linking |
LINEITEMS_GET | For the list of grade lines | Line items |
LINEITEMS_POST | To create a grade line | Line items |
LINEITEM_GET | For one grade line | Line items |
LINEITEM_PUT | To update a grade line | Line items |
LINEITEM_DELETE | To delete a grade line | Line items |
SCORE_POST | To record a grade | Scores and results |
RESULTS_GET | For recorded grades | Scores 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.
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
📄️ Memberships
Answer roster requests from tools through the Names and Roles service.
📄️ Line Items
Let tools create and manage grade lines in your gradebook.
📄️ Scores and Results
Record the grades tools post, and return them when asked.
