Receiving launches
The launch receiver is the HTTPS endpoint that receives people — the Callback URL you provide when registering your app. When a student (or instructor) opens your app 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.
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 namedlaunch_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 app, so show whatever they should see: render your page directly, or redirect them onward based on the location that was launched.
A typical receiver:
- Verify the JWT (signature + expiry).
- 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.
- Store what you'll need later:
launch_id(required to send a grade),user.id,user.member, thestoragerow URLs, and theapitoken if you plan to use the App Data API. - Record the school, course, person, and membership if you haven't seen them before — see What a launch tells you about.
- Switch on
location.extidto 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["launch_id"] = data["launch_id"]
session["can_grade"] = data["grade_passback"]
session["storage"] = data["storage"]
session["api"] = data["api"]
record_launch(data) # see What a launch tells you about
return route_to_screen(data["location"])
Course setup requests don't arrive here. When a course or a location needs setting up in your app, 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 app. See How provisioning works.
What a launch tells you about
Voshi never tells your app 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 new | Recognize it by | When it happens |
|---|---|---|
| A school | an unseen organization.deployment | Someone at a new institution launches your app for the first time. |
| A course | an unseen context | The first launch of any placement in that course. |
| A person | an unseen user.id | Someone who has never opened your app at this school. |
| A membership | an unseen user.member | A person you already know, launching from a course you haven't seen them in. |
| A placement | an unseen resource_link | An instructor placed a location you already support. |
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 app'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.
Whatever store you use, write these as upserts, not check-then-insert. A student who double-clicks the course link, or opens your app 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.nameandcourse.labelchange when the institution renames or re-terms the course.location.labelandlocation.typechange 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 onlocation.extid.user.groupschanges 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 apps 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 app being told. See Placement.
Nothing is ever torn down
Launches are the only event your app 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.
Using App Data 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.groupsbefore 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.memberand 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
extidfrom 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_passback: falseon a graded location. A location of typeassessmentcan still launch without grade passback 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 app 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.