Skip to main content
Version: 2026-08-25 (archived)

Quickstart

This walkthrough takes you from nothing to a working Voshi app: the three HTTPS endpoints Voshi calls, 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 each URL you register is public https://).
  • A Voshi builder account — contact the MyEducator team if you don't have dashboard access yet.
Register your app

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.

Your app also needs a Provision URL and a Locations URL (steps 3 and 4). Set both on the app's Profile tab after registering — see Registering your app.

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.

warning

The API key is shown only once. If you lose it, rotate it with the current key, or contact the MyEducator team if you no longer have it.

Build your launch receiver

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:

# 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["voshi_member_id"] = data["user"]["member"] # this person in this course
session["groups"] = data["user"]["groups"] # e.g. ["instructor"]
session["course_id"] = data["context"]
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']}"

Verify, read the claims, serve your app. See Verifying the launch JWT for the full security checklist — every endpoint below verifies launch_data the same way.

Serve your locations

Instructors can only place destinations your app tells Voshi about. When one is placing content, Voshi POSTs a signed launch_data to your Locations URL and shows them what you return, keyed by an external ID of your own choosing:

@app.post("/voshi/locations")
def voshi_locations():
verify_launch_data(request.form["launch_data"]) # same verification as a launch

return {"locations": {
"ch3_quiz": {
"type": "assessment", # the only type that can be graded
"label": "Chapter 3 Quiz",
"description": "20 questions on normal distributions.",
"points": 50, # gradebook column value
},
"syllabus": {"type": "content", "label": "Course Syllabus"},
}}

Build this list from your own data and it stays current on its own. See Serving your locations.

Set courses up when Voshi asks

Before the first launch of a course reaches you, Voshi asks your app to set that course up — and the same for each location placed in it. Answer 204 immediately, then call the URLs from the provision claim when you're done:

@app.post("/voshi/provision")
def voshi_provision():
data = verify_launch_data(request.form["launch_data"])
queue_setup(data) # do the work in the background
return "", 204 # answer first

def queue_setup(data):
headers = {"Authorization": f"Bearer {data['api']['token']}"}
# The course first — a location cannot be provisioned before its course.
if "context" in data["provision"]:
requests.put(data["provision"]["context"], headers=headers).raise_for_status()
if "location" in data["provision"]:
requests.put(data["provision"]["location"], headers=headers).raise_for_status()

Until those calls land, launches for that course wait — and its context and location storage rows stay closed. See How provisioning works.

Route on the location

The location claim tells you which destination was launched — your own extid, Voshi's id, plus its type and label. There is no params object; configuration lives in your own data, keyed on the extid.

Switch on location.extid to decide what to render:

location = data["location"]
if location["extid"] == "ch3_quiz":
quiz = Quiz.query.filter_by(extid=location["extid"]).one()
Send a grade back

If the launch arrived with "grade_passback": true, you can push a score to the LMS gradebook. Authenticate with your API key:

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

See Grade passback for retry behavior and sync statuses.

Go live

Your app starts in draft status. Draft apps can't be placed in courses or launched from an LMS, so once all three endpoints and your 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.

The dashboard's Test tab exercises your Locations URL, your Callback URL, and grade passback against fabricated claims before any of that — the quickest way to check your JWT verification. Trial requests carry test: true and record nothing.

Where to next​