Quickstart
This walkthrough takes you from nothing to a working Voshi app: one HTTPS endpoint that receives launches, plus one API call that pushes a grade back to the LMS.
What you'll need
- A place to run an HTTPS web server that's reachable from the public internet (Voshi validates that your callback URL is public
https://). - A Voshi builder account — contact the MyEducator team if you don't have dashboard access yet.
Open the Voshi dashboard, go to Apps → Register App, and fill in:
- App Name — shown to instructors when they add your app to a course.
- Callback URL — the HTTPS endpoint you're about to build (e.g.
https://myapp.example.com/launch_receiver). - Description / Logo URL — optional.
The response shows your API key exactly once — copy it now and store it securely, like a password. It authenticates your calls to the grades and management APIs.
The API key is shown only once. If you lose it, contact the MyEducator team.
When someone opens your app from an LMS, Voshi sends their browser to your callback URL with a form POST containing a single field, launch_data — a signed JWT carrying everything about the launch.
A minimal receiver that verifies the JWT and starts a session:
- Python (Flask)
- Node (Express)
# pip install flask pyjwt cryptography
from flask import Flask, request, session
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://api.link.voshi.com/lti13/v1/jwks"
jwks = PyJWKClient(JWKS_URL) # fetches and caches Voshi's public keys by kid
app = Flask(__name__)
app.secret_key = "replace-with-a-real-secret"
@app.post("/launch_receiver")
def launch():
token = request.form["launch_data"]
# Verify the signature and expiry — never trust the claims before this.
signing_key = jwks.get_signing_key_from_jwt(token)
data = jwt.decode(token, signing_key.key, algorithms=["RS256"])
# Trade the one-time launch token for your own session.
session["voshi_user_id"] = data["user"]["id"]
session["role"] = data["user"]["role"]
session["course_id"] = data["course"]["id"]
session["launch_id"] = data["launch_id"] # needed for grade passback
session["can_grade"] = data["grade_passback"]
location = data["location"]
if location["type"] == "assessment":
return f"Quiz time! You launched: {location['label']}"
return f"Welcome to {location['label']}"
// npm install express jose express-session
import express from 'express'
import session from 'express-session'
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS_URL = 'https://api.link.voshi.com/lti13/v1/jwks'
const jwks = createRemoteJWKSet(new URL(JWKS_URL)) // caches keys by kid
const app = express()
app.use(express.urlencoded({ extended: false }))
app.use(session({ secret: 'replace-with-a-real-secret', resave: false, saveUninitialized: false }))
app.post('/launch_receiver', async (req, res) => {
// Verify the signature and expiry — never trust the claims before this.
const { payload: data } = await jwtVerify(req.body.launch_data, jwks, {
algorithms: ['RS256'],
})
// Trade the one-time launch token for your own session.
req.session.voshiUserId = data.user.id
req.session.role = data.user.role
req.session.courseId = data.course.id
req.session.launchId = data.launch_id // needed for grade passback
req.session.canGrade = data.grade_passback
const location = data.location
if (location.type === 'assessment') {
res.send(`Quiz time! You launched: ${location.label}`)
} else {
res.send(`Welcome to ${location.label}`)
}
})
app.listen(3000)
That's the whole integration surface: verify, read the claims, and serve your app. See Verifying the launch JWT for the full security checklist.
The location claim tells you which destination in your app was launched — its id, type, label, and the static params you configured on it in the dashboard.
Create one location per destination you want instructors to place — e.g. a Chapter 3 Quiz location of type assessment with chapter=3. Then switch on location.id or its params to decide what to render:
chapter = data["location"]["params"].get("chapter") # params are always strings
If the launch arrived with "grade_passback": true, you can push a score to the LMS gradebook. Authenticate with your API key:
- Python
- Node
import requests
resp = requests.post(
"https://api.link.voshi.com/ltiaas/v1/grades",
headers={"Authorization": "Bearer ltiaas_myappid_mysecret"},
json={
"launch_id": session["launch_id"],
"score": 0.85, # a fraction from 0.0 to 1.0
"comment": "Nice work!",
},
)
resp.raise_for_status()
print(resp.json()["sync_status"]) # "synced" if it reached the gradebook
const resp = await fetch('https://api.link.voshi.com/ltiaas/v1/grades', {
method: 'POST',
headers: {
Authorization: 'Bearer ltiaas_myappid_mysecret',
'Content-Type': 'application/json',
},
body: JSON.stringify({
launch_id: req.session.launchId,
score: 0.85, // a fraction from 0.0 to 1.0
comment: 'Nice work!',
}),
})
const grade = await resp.json()
console.log(grade.sync_status) // "synced" if it reached the gradebook
See Grade passback for retry behavior and sync statuses.
Your app starts in draft status. Draft apps can't be placed in courses or launched from an LMS, so once your launch flow and grade passback work, contact the MyEducator team to activate your app. After activation, instructors can add it to their courses and you can test end-to-end from a real LMS.