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

Receiving launches

The launch receiver is the HTTPS endpoint that receives people — the Callback URL you provide when registering your tool. When a student (or instructor) opens your tool from the LMS, Voshi sends the user's browser to your endpoint with an HTTP POST describing the launch.

It's one of three endpoints you build. The other two — your Provision URL and your Locations URL — are called by Voshi's server, never by a person's browser, and neither of their requests arrives here.

The request Voshi sends you​

Your callback URL receives a form POST with a single field: a signed JWT carrying the data from this launch.

POST /launch_receiver HTTP/1.1
Host: myapp.example.com
Content-Type: application/x-www-form-urlencoded

launch_data=eyJhbGciOiJSUzI1NiIsImtpZCI6...

Verify its signature and decode it to read the launch — see Verifying the launch JWT.

warning

Because the JWT arrives through the browser rather than from a Voshi server, its signature is the only thing standing between you and a forged launch. Never trust the claims until you've verified the signature.

Requirements for your endpoint​

  • Public HTTPS. The callback URL must be an absolute https:// URL on a publicly-routable host. Voshi validates this when you register and again at every launch — URLs pointing at private or loopback addresses are rejected.
  • Accept a form POST. Content-Type: application/x-www-form-urlencoded, one field named launch_data.
  • Be fast. The user is sitting in front of a blank-ish redirect page while your endpoint responds. Do the minimum — verify, set a session cookie, render or redirect.

What happens after the POST​

From this point, the user is simply navigating your tool, so show whatever they should see: render your page directly, or redirect them onward based on the location that was launched.

A typical receiver:

  1. Verify the JWT (signature + expiry).
  2. Create your own session for the user — the launch token is a one-time entry ticket, not a session credential. Don't accept the same token twice.
  3. Store what you'll need later: user.id, user.member, the storage row URLs, grade.submit (the URL you'll send a score to, or null), and the api token if you plan to use the Storage API.
  4. Record the school, course, person, and membership if you haven't seen them before — see What a launch tells you about.
  5. Switch on location.extid to decide which screen to render.
@app.post("/launch_receiver")
def launch():
data = verify_launch_jwt(request.form["launch_data"]) # see Verifying the JWT

session["voshi_user_id"] = data["user"]["id"]
session["voshi_member_id"] = data["user"]["member"]
session["groups"] = data["user"]["groups"]
session["grade_url"] = data["grade"]["submit"] # None when this placement can't be graded
session["storage"] = data["storage"]
session["api"] = data["api"]

record_launch(data) # see What a launch tells you about

return route_to_screen(data["location"])
note

Course setup requests don't arrive here. When a course or a location needs setting up in your tool, Voshi's server calls your Provision URL instead, with a different set of claims. A request to your Callback URL is always a person opening your tool. See How provisioning works.

What a launch tells you about​

Voshi never tells your tool about a school, course, or person ahead of time. There is no roster sync, no admin onboarding step, and no API that hands you a list of users to create. A launch is the first and only notice you get that any of them exist — and it arrives with the student already waiting.

So treat your receiver as a record-and-refresh step, not a lookup. Any of these can be new on any launch, in any combination:

What may be newRecognize it byWhen it happens
A schoolan unseen organization.deploymentSomeone at a new institution launches your tool for the first time.
A coursean unseen contextThe first launch of any placement in that course.
A personan unseen user.idSomeone who has never opened your tool at this school.
A membershipan unseen user.memberA person you already know, launching from a course you haven't seen them in.
A placementan unseen resource_linkAn instructor placed a location you already support.
note

user.id is stable per school. The same human at two institutions arrives as two different user.ids, and nothing in the launch lets you connect them — there's no email or name to match on. Treat people as belonging to their organization.

This is your tool's own bookkeeping, and it is not the same thing as provisioning — that's a separate, explicit protocol for setting a course up, driven by Voshi calling your Provision URL. This bookkeeping runs on every launch; provisioning happens once per course and once per location.

warning

Whatever store you use, write these as upserts, not check-then-insert. A student who double-clicks the course link, or opens your tool in two tabs, produces two simultaneous first launches — a read followed by a write races itself, and the loser surfaces to the student as a broken launch.

Re-sync the fields that change​

Recording on first launch isn't enough — some claims change after you've seen them, and the launch is your only chance to notice:

  • course.name and course.label change when the institution renames or re-terms the course.
  • location.label and location.type change when your locations endpoint serves different values and someone places that destination again — Voshi captures the copy at placement time. Neither the old nor the new value is authoritative for you: treat these as display text and read real configuration from your own data, keyed on location.extid.
  • user.groups changes when someone's role in the course changes — a student promoted to TA, for example.

A find_or_create that skips the update goes quietly stale. Write these on every launch.

Groups belong to the membership​

user.groups describes this person in this course, on this launch — not the person. The same user.id is legitimately a student in one course and an instructor in another, and either can change between launches. Store the groups against user.member, never against user.id, and refresh them every time. Deciding what to show from a role you cached at signup is the most common way tools end up showing instructor tools to a student.

Check membership rather than equality — "instructor" in groups, not groups[0] == "instructor" — since a person can hold several groups. See the groups reference for the full list.

Key your own data on extid​

Your records for a location belong to location.extid, not to resource_link and not to location.id. An extid is the same "thing" in every course and every term, while resource_link identifies one instructor's one link — and a course can end up with more than one link to the same location without your tool being told. See Placement.

Nothing is ever torn down​

Launches are the only event your tool receives about people. You will never be told that a student dropped the course, that an instructor deleted a link, or that a term ended — those things simply stop producing launches. Don't design anything that waits for a teardown signal.

If staleness matters to you — seat counts, active-course reporting, data retention — derive it from activity instead: stamp a last_seen_at on every launch and age records out on your own schedule.

Keep the launch fast​

Bookkeeping is the usual reason a receiver gets slow, and the user is watching a blank page while it runs. Keep it to the cheap writes above. Push welcome emails, analytics events, content generation, and calls to external services into a background job, and render as soon as the records the first screen needs exist. Course-level setup work belongs in your provisioning handler, which runs before students arrive and isn't on anyone's critical path.

note

Using Storage storage? Then most of this is already done for you: the member and member_location rows are open from the first launch, so there's nothing to create and no schema to keep in sync. The context and location rows additionally require that the course be provisioned.

Launches you might not expect​

Plan for a few launch shapes beyond the happy path:

  • Instructor launches. Instructors launch the same locations students do — check user.groups before showing student-only or instructor-only UI.
  • Repeat launches. The same user launching the same placement again is normal (they clicked the course link again). Recognize them by user.member and resume where they left off — upserting on every launch keeps this on the same code path as their first one.
  • A location you stopped serving. Links outlive your catalog. Dropping an extid from your locations list only hides it from the instructor's picker — every link already placed keeps launching, forever. Keep serving your locations. Retiring one breaks courses that are still using it, and you get no warning that it happened. If you truly must, keep handling the launch and render your own "no longer available" screen rather than erroring — see Extid.
  • grade.submit: null on a graded location. A location of type assessment can still launch with nowhere to post a score, if the LMS didn't create a gradebook column for the placement. Degrade gracefully — see Grade passback.

Errors on Voshi's side​

If a launch can't be forwarded — for example, the tool is still in draft status, or the callback URL fails validation — Voshi shows the user an error page with a report ID. Your endpoint is never called, so you don't need to handle these; they show up as instructor reports instead.

Two other failures also happen entirely outside your receiver:

  • The course isn't provisioned yet. Voshi holds the launch and shows a "not set up yet" or "Provisioning" page instead of calling you. See How provisioning works.
  • Your locations endpoint failed while an instructor was placing content. They see a deep-link error and place nothing. See When this endpoint fails.