Changelog
Changes to what Voshi sends your tool or expects from it, newest first. Each entry says what you have to change to keep working.
The docs as they stood before each change stay published — pick the date from the version dropdown at the top of any page, or follow the links below.
2026-10-05 — Hosted tools are the main path, and rows keep files
No action required for a self-hosted tool. Nothing it receives or must send has changed, and one claim was added. No snapshot was cut.
The docs are organized around hosted tools
Most tools are built in the dashboard's AI Builder, or uploaded as a folder of browser code, and run on Voshi's hosting. These docs now describe that path first: how a hosted tool works, the dashboard, a tool's files and manifest.json, locations, the Sandbox, versions, and the Voshi API a hosted tool is handed — launch details, storage, files, grades, assets, and errors.
Everything about running your own server — the three endpoints, the signed launch_data JWT, provisioning, the locations endpoint, and the REST APIs for storage and grades — is unchanged and now lives under Advanced: your own server. Every page moved, so update any bookmarks; the content is the same.
The docs also now say tool where they used to say app. The API is unchanged: the app claim, the /apps/… URL segments, and the app section of manifest.json keep their names.
New claim: files
Every launch (and provision request) now carries a files object beside storage, with the same four keys. Each is the URL of the files kept beside that storage row — for data too large for the row's JSON object or not JSON at all. GET it to list the row's files; <url>/<key> reads, stores, or removes one. Who may read and write a row's files follows the row's own rules.
- See the
filesclaim and the file operations in the Storage API reference. - A hosted tool reaches the same files as
api.storage.<scope>.files— see Files.
Storage rows have a documented size limit
The JSON object of a member or member_location row is limited to 512 KB, and of a context or location row to 1 MB, as compact JSON; a larger write is refused with 422. Earlier pages said no fixed limit was enforced. Nothing changed on the server; the limit is now stated. Larger data belongs in the row's files.
The Sandbox has a gradebook
The dashboard's Test tab is now the Sandbox, and its trial course has a gradebook of its own: an assessment placed there through the picker arrives with a grade.submit URL, and scores you publish show on the Sandbox's Grades tab. Earlier pages said grade passback could not be tested there. The Launch Logs and Grade Logs tabs are now Launches and Grades.
2026-09-21 — Grade passback, and the end of the management API
Action required. Grade passback is a different call now, one launch claim was replaced, and every /apps/me endpoint is gone. The contract before this change is archived at 2026-08-25.
Grades: post to the URL the launch gives you
POST /ltiaas/v1/grades has been removed. Grades are no longer keyed on a launch at all — they are keyed on the student and the location, which is what a gradebook column actually belongs to.
- Every launch now carries a
grade.submitURL.POSTyour score there. It's ready to call, like thestorageURLs, and it already names the course, the student, and the location — the body is just the score. - The body loses
launch_id;score,max_score,comment,activity_progress, andgrading_progressare unchanged. - Every submission is an attempt, and nothing is overwritten. There is no longer one grade per launch that resubmission updates: each
POSTpublishes to the LMS and records a new attempt. To regrade or retry, post again. - The response is that attempt.
sync_status/sync_error/synced_at/grade_id/launch_idare gone; checkstatus(successorfailed) and readdata.errorwhen it failed. A refused score is still200. GET /ltiaas/v1/grades/{grade_id}is removed with no replacement. The response to eachPOSTis the record — log what you need. Your tool's history is in the dashboard under Grade Logs.- Grading someone other than the launching user is now first-class: swap the member segment of a
/contexts/…grade URL for another student'suser.member. An instructor grading a roster, or a job scoring work offline, no longer needs a launch for each student — and a score can be published for a student who never opened the activity.
See Grade passback and the API reference.
Changed claim
| Before | Now | What to do |
|---|---|---|
grade_passback (boolean) | grade.submit (URL or null) | Replace the boolean check with a null check, and post the score to the URL. null in exactly the cases the boolean was false. |
launch_id is still sent, but nothing in the API is keyed on it any more. You no longer need to store it to grade later — store grade.submit instead.
The management API is gone
GET/PATCH /ltiaas/v1/apps/me, the four apps/me/locations endpoints, and POST /apps/me/rotate-key have all been removed, and your API key no longer authorizes anything but grade submission. Requests to those paths now 404.
Change your tool's name, URLs, description, and logo on the dashboard's Settings tab, and rotate your key there too. If deployment automation depended on PATCH /apps/me, it needs changing — and the MyEducator team would like to hear about it. See Managing your tool.
Registration asks where your tool runs
New tools choose Voshi Hosted or Self Hosted at registration, and the choice is permanent. Self-hosted is what these docs describe: your own server, your three URLs, your API key. A hosted tool is built in the dashboard and gets no API key, since it has no server to keep one on. callback_url is no longer accepted in the registration call itself — set it on the Settings tab afterwards, like the other two URLs.
The Test tab now runs real launches
The dashboard's Test tab used to send fabricated claims carrying test: true and record nothing. It now launches your tool for real, in a private trial course of its own: your locations endpoint is asked through the ordinary content picker, your provisioning endpoint is called and waited on, real storage rows are created, and your callback URL gets a properly signed launch.
- If your code branches on a
testclaim, that branch is now dead — nothing sends one. - Trial launches have no gradebook, so they arrive with
grade.submit: null. Grade passback is tested from a real course. - A test link can be Reset, which discards everything your tool stored at it so the next launch provisions again.
See Testing your tool.
Migration checklist
- Store
grade.submitfrom each launch; stop storinglaunch_idfor grading. - Replace
grade_passbackboolean checks with a null check ongrade.submit. - Repoint grade submissions from
POST /ltiaas/v1/gradesto that URL, and droplaunch_idfrom the body. - Check
statuson the response instead ofsync_status; drop any polling ofGET /grades/{grade_id}. - Replace resubmit-to-update logic with plain resubmission — it is a new attempt, and that is correct.
- Remove any calls to
/apps/meendpoints; do that configuration in the dashboard. - Delete any
test: truebranch in your locations or launch handling.
2026-09-04 — Messages and grouped locations
No action required. Both additions are optional, and a tool that answers the locations request exactly as it does today keeps working unchanged. No snapshot was cut: nothing about the old contract stopped being true.
Your locations response can carry a message
Send a message alongside (or instead of) your locations and Voshi shows it to the instructor above the picker. It is the answer to the empty picker: when you offer nothing, you are the only party who knows why — an email that matches no account, an institution that has adopted nothing — and until now there was no way to say so.
- Shown as plain text; markup is stripped, and it is truncated at 2000 characters.
- Valid on its own:
{"message": "…"}with no locations is a complete answer. - See
message.
Your locations can be grouped
A new grouped_locations key arranges locations into named, collapsible sections, nested up to 5 levels. Instructors see them collapsed, open what they want, and can take a whole group at once or pick from several groups in one placement.
- A group needs a
label, and it has to be a real one: an empty label, or one that is only whitespace or markup, is refused. It may also carrydescription,locations, and its owngrouped_locations— the same key again, since a group holds locations the way the response itself does. - Grouping is presentation only — it is not part of an extid, is not stored on the location, and never appears in a launch, so you can reorganize freely without disturbing content that is already placed.
- Locations sent at the top level are shown above the groups, uncollapsed.
- See
grouped_locations.
One thing that is now refused
A locations response naming none of locations, grouped_locations, or message is rejected as an invalid structure. Previously locations was required, so a response without it was already refused; this only changes the wording of the error. To answer "nothing right now", send a message, an empty locations object, or both.
2026-09-02 — Documentation corrections
No contract change. Nothing about what Voshi sends or accepts changed on this date; these are places where the docs described the platform wrongly. No snapshot was cut, because there is no earlier contract to strand anyone on. Check each item against your integration.
Claims
location.paramsdoes not exist and never did on this contract. Earlier pages showed aparamsobject on the launch and provisionlocationclaim, documented it as a per-location field on your locations response, and suggested a "few parameterized destinations" strategy built on it. Voshi sends no such object and stores no such field. If you wrote code readinglocation["params"], it has been failing or silently reading nothing — move that configuration into the extid or your own database. See There are no per-location params.- Launches to course staff carry name and email.
user.given_name,user.family_name,user.full_name, anduser.emailare populated whengroupscontainsmanager,instructor, orassistant, and arenullotherwise. Earlier pages said flatly that no PII is forwarded. Student launches are unchanged — still no PII. Seeuser. - A provision request does carry
course. It was listed among the claims a provision request does not have. It has always been sent, in the same shape as a launch's.
Tool data permissions
The who-can-access-what rules were wrong in both directions:
- Students cannot write the shared
contextandlocationrows — they have read access only, and a write returns403. Earlier pages warned that students could overwrite them. - Course staff can read and write any member's
memberandmember_locationrows in their course. Earlier pages said only the user themself could reach their own rows. - Tool builders get no automatic access to any row. Being a member of the tool in the dashboard grants nothing; a builder has to be enrolled in the course to read data there.
APIs
POST /gradesacceptsmax_score, which rewrites the gradebook column's points possible before publishing the score, and returns it on the grade object. Undocumented until now — see the request body.provision_urlandlocations_urlare readable and writable throughGET/PATCH /apps/me, and settable in the dashboard. (The/apps/meendpoints were removed on 2026-09-21; the dashboard tab is now called Settings.)POST /apps/me/rotate-keyissues a new API key and invalidates the old one. (Removed on 2026-09-21 — rotate from the dashboard instead; see rotating your key.)- The legacy location object is
{id, extid, type, label}, andextidis required when creating one. Earlier pages showedis_default,order, andparamsinstead.
Dashboard
The tool detail tabs are Overview, Profile, Locations, Test, Launches, Grade Logs. Pages referring to a Settings tab meant Profile, and there is no Members tab — ask the MyEducator team to change who manages a tool. (Renamed since: the tabs are now Overview, Settings, Locations, Pricing, Test, Launch Logs, Grade Logs.)
2026-08-25 — Provisioning and app-served locations
Action required. Your tool now builds three endpoints instead of one, and one claim was replaced. The contract before this change is archived at 2026-08-19.
New: your tool serves its own locations
Locations are no longer defined in the dashboard. Voshi asks your tool for its destinations whenever an instructor is placing content, and you identify each one with an external ID of your choosing.
- Register a Locations URL and answer it with
{"locations": {"<extid>": {…}}}— see Serving your locations. - A tool with no Locations URL cannot be placed in a course. This is the change most likely to stop a working integration.
- Launches now carry both
location.extid(yours) andlocation.id(Voshi's). Switch onextid. - The dashboard's Locations editor and the
apps/me/locationsAPI still exist but no longer drive what instructors see.
New: provisioning
Before the first launch of a course reaches you, Voshi asks your tool to set that course up.
- Register a Provision URL, answer
204promptly, thenPUTeach URL in theprovisionclaim when that level is ready — see How provisioning works. - The
contextandlocationstorage rows now answer422 … has not been provisioneduntil you finish.memberandmember_locationare unaffected. - Voshi no longer copies course data into a copied course. A provision request for a copied course hands you the previous course's row URLs (
storage.parent_context,storage.parent_location) and you carry forward what matters — see Copied courses.
Changed claims
| Before | Now | What to do |
|---|---|---|
user.role (student / instructor / admin) | user.groups (a list) and user.member | Replace role comparisons with membership checks ("instructor" in groups). Key enrollment records on user.member. See user. |
resource_link_id | resource_link | Rename. Same meaning. |
organization.id | organization.deployment | Rename — deployment always held the same value. |
| — | context | New: the course ID on its own, matching the storage URLs and used by provisioning. course still carries the display text. |
| — | location.extid | New: your own ID for the launched destination. |
Corrections to these docs
Behavior that didn't change, but that earlier pages described wrongly:
issis always Voshi (https://api.link.voshi.com), not the school's LMS. If you configured your JWT library to expect the LMS issuer, it was never matching what we send.- Storage URLs are on
api.link.voshi.com. Examples showedlink.voshi.com. - A launch's
locationvalues are the copy Voshi captured when the placement was made, not your current list. Earlier pages said edits applied immediately to existing placements. Drive your tool offlocation.extidand your own data.
Migration checklist
- Replace
user.rolereads withuser.groupsmembership checks; store groups againstuser.member. - Rename
resource_link_id→resource_linkandorganization.id→organization.deployment. - Build and register your Locations URL — without it your tool can't be placed.
- Build and register your Provision URL, and call the provision URLs when setup finishes.
- If you relied on Voshi copying course data across terms, move that into your provisioning handler.
- If you pinned
issto the LMS issuer, pin it tohttps://api.link.voshi.cominstead.