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

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.
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.

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, contact the MyEducator team.

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["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']}"

That's the whole integration surface: verify, read the claims, and serve your app. See Verifying the launch JWT for the full security checklist.

Route on the location

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
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 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.

Where to next​