Skip to main content
Version: 2026-09-21 (current)

Your own server

Most tools are hosted by Voshi: the builder writes browser code, and Voshi runs everything else. A self-hosted tool runs on your own server instead. Voshi still handles the LMS, the placement, the students, and the gradebook, but it hands each launch to your server over HTTPS, and your server does the rest.

This section is the contract for that: the endpoints you build, what arrives at them, how to verify it, and the REST APIs for storage and grades. If you are building a hosted tool, none of it applies to you — everything in it is done for you by the Voshi API.

When to self-host​

Choose your own server when the tool needs something that cannot live in the browser:

  • A backend of your own — an existing product, a database with its own schema, server-side logic.
  • Grading that must not be readable off the tool's source, computed and published from your server.
  • A catalog of locations that changes dynamically — locations generated per placement, or served from your own data.
  • Grading outside the launch — a background job, an instructor's roster view, scoring work hours after the student left.

Everything else — identity, storage, files, grades during the launch — a hosted tool already has, with no server to run.

Hosted or self-hosted​

The choice is made at registration and cannot be changed afterwards. It decides whether your tool has endpoints of its own at all.

Voshi HostedSelf Hosted
Where your code runsVoshi's CDN, in the learner's browserYour own server
How you build itAI Builder, or upload a folderHowever you like
LocationsDeclared in the dashboard or manifest.jsonServed by your Locations URL on demand
Course setupAutomaticYour Provision URL is called
LaunchesYour element is constructed with the Voshi APIA signed launch_data JWT is posted to your Callback URL
Storage and gradesapi.storage, api.submitGrade()REST calls with the launch's token, or your API key
API keyNoneOne, shown once

The three endpoints you build​

Voshi reaches your tool at three URLs. All three must be public HTTPS, and all three receive a form POST with a single signed launch_data field that you verify the same way.

URLCalled whenDocs
Callback URLSomeone opens your tool from a course. Arrives through their browser.Receiving launches
Provision URLA course, or a location in it, needs setting up in your tool. Server to server.Receiving provision requests
Locations URLAn instructor is placing your tool and Voshi needs your list of destinations. Server to server.Serving your locations

Only the Callback URL is strictly required to receive a launch — but a tool without a Locations URL can't be placed in a course, and a tool without a Provision URL can't finish course setup, so a live tool needs all three.

Register in the dashboard​

Tools → New Tool

In the dashboard, fill in the Tool Name and choose Self Hosted under Hosting. This cannot be changed later.

Save your API key

Registration shows your API key exactly once — copy it now and store it securely, like you would a password. Its one job is letting your server publish grades with no launch session.

Register your three URLs

Open your tool's Settings tab and fill in Callback URL, Provisioning URL, and Locations URL. They're blank at registration on purpose — the server they point at usually doesn't exist yet when you register.

You can edit the name, all three URLs, the description, and the logo at any time on the Settings tab. Changes apply immediately — including to placements instructors have already made. See Managing your tool.

Endpoint URL rules​

Each of the three URLs is validated when you set it and re-checked every time Voshi calls it:

  • Must be an absolute https:// URL (no http://, no relative paths).
  • Must point at a publicly-routable host — URLs that resolve to private, loopback, or link-local addresses are rejected. Your local machine can't receive launches; use a tunnel with a public HTTPS hostname or a deployed environment during development.

A minimal self-hosted tool​

Build your launch receiver

When someone opens your tool, 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. Verify it, start your own session, and render.

# 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["grade_url"] = data["grade"]["submit"] # None if this placement can't be graded

location = data["location"]
if location["type"] == "assessment":
return f"Quiz time! You launched: {location['label']}"
return f"Welcome to {location['label']}"

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 tool 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"},
}}

See Serving your locations.

Set courses up when Voshi asks

Before the first launch of a course reaches you, Voshi asks your tool 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. See How provisioning works.

Send a grade back

Every launch carries a grade.submit URL — or null, when that placement has no gradebook column. POST the score there; your API key authorizes it with no launch session:

import requests

grade_url = session["grade_url"]
if grade_url: # None when this placement can't be graded
resp = requests.post(
grade_url,
headers={"Authorization": "Bearer ltiaas_mytoolid_mysecret"},
json={
"score": 0.85, # a fraction from 0.0 to 1.0
"comment": "Nice work!",
},
)
resp.raise_for_status()
attempt = resp.json()
print(attempt["status"]) # "success" if it reached the gradebook

A rejected score comes back 200 with status: "failed" and the reason in data.error — check it. See Grade passback.

Testing your tool​

The dashboard's Sandbox tab launches your tool in a trial course of its own, before any LMS is involved and while your tool is still in draft status. These are real launches: Voshi plays the part of the LMS, your Locations URL is asked what you offer through the ordinary picker, your Provision URL is called and waited on, your Callback URL receives a properly signed launch_data, real storage rows are created, and the Sandbox has a gradebook of its own that your grade submissions land in.

  • New Link opens your tool's content picker, served from your Locations URL, and what you pick becomes a test link.
  • Launch any test link as a student or as an instructor. Each role is a separate person in the trial course with its own data.
  • Reset on a test link throws away everything stored for that link, so the next launch provisions it from scratch — the way to re-test your Provision URL.
  • The Sandbox's Launches and Grades tabs show what Voshi recorded.
note

A trial launch signs you into the browser as the test student or instructor, not as yourself. Open launches in a new window, and expect to sign back into the dashboard afterwards.

Tool status​

StatusMeaning
draftThe initial status. The tool cannot be placed in real courses or launched from an LMS — it's invisible to instructors. You can still test it from the Sandbox.
activeLive: instructors can place it, launches are forwarded to your Callback URL.
suspendedDisabled by the platform team. Launches and API calls are rejected.

Only the Voshi platform team can change your tool's status. The path to going live is:

  1. Build your three endpoints and your grade flow against these docs.
  2. Exercise them from the Sandbox tab.
  3. Contact the MyEducator team to activate your tool.
  4. Test from a real LMS course — place a location, launch as an instructor and as a student, and verify grades land in the gradebook.

Your API key​

Only a self-hosted tool has one. It has the form:

ltiaas_<tool-id>_<secret>
  • It's shown once, at registration. Voshi stores only a hash of it and cannot show or recover it — the dashboard displays a hint (ltiaas_…last4) so you can tell which key is live.
  • Keep it server-side only.
  • Its one use is publishing a grade from your own server, long after the launch session is gone. It does not configure your tool, and it is not a general-purpose credential.
  • It is not involved in launch verification — launches are verified against Voshi's public keys. Nor is it what provisioning calls use; those authenticate with the request's own api.token.

Rotating or replacing your API key​

Rotate the key from the Settings tab in the dashboard. A new key is issued and shown once, exactly as at registration.

warning

The old key stops working immediately — there is no overlap window. Every deployed instance of your tool still using it starts getting 401 the moment you rotate, so roll the new key out promptly, or accept a gap in server-side grading.

Rotation is also the only recovery for a lost key, and the right response to a leaked one.

Where to next​