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

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 files claim 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.submit URL. POST your score there. It's ready to call, like the storage URLs, 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, and grading_progress are unchanged.
  • Every submission is an attempt, and nothing is overwritten. There is no longer one grade per launch that resubmission updates: each POST publishes 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_id are gone; check status (success or failed) and read data.error when it failed. A refused score is still 200.
  • GET /ltiaas/v1/grades/{grade_id} is removed with no replacement. The response to each POST is 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's user.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​

BeforeNowWhat 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 test claim, 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​

  1. Store grade.submit from each launch; stop storing launch_id for grading.
  2. Replace grade_passback boolean checks with a null check on grade.submit.
  3. Repoint grade submissions from POST /ltiaas/v1/grades to that URL, and drop launch_id from the body.
  4. Check status on the response instead of sync_status; drop any polling of GET /grades/{grade_id}.
  5. Replace resubmit-to-update logic with plain resubmission — it is a new attempt, and that is correct.
  6. Remove any calls to /apps/me endpoints; do that configuration in the dashboard.
  7. Delete any test: true branch 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 carry description, locations, and its own grouped_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.params does not exist and never did on this contract. Earlier pages showed a params object on the launch and provision location claim, 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 reading location["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, and user.email are populated when groups contains manager, instructor, or assistant, and are null otherwise. Earlier pages said flatly that no PII is forwarded. Student launches are unchanged — still no PII. See user.
  • 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 context and location rows — they have read access only, and a write returns 403. Earlier pages warned that students could overwrite them.
  • Course staff can read and write any member's member and member_location rows 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 /grades accepts max_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_url and locations_url are readable and writable through GET/PATCH /apps/me, and settable in the dashboard. (The /apps/me endpoints were removed on 2026-09-21; the dashboard tab is now called Settings.)
  • POST /apps/me/rotate-key issues 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}, and extid is required when creating one. Earlier pages showed is_default, order, and params instead.

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) and location.id (Voshi's). Switch on extid.
  • The dashboard's Locations editor and the apps/me/locations API 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 204 promptly, then PUT each URL in the provision claim when that level is ready — see How provisioning works.
  • The context and location storage rows now answer 422 … has not been provisioned until you finish. member and member_location are 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​

BeforeNowWhat to do
user.role (student / instructor / admin)user.groups (a list) and user.memberReplace role comparisons with membership checks ("instructor" in groups). Key enrollment records on user.member. See user.
resource_link_idresource_linkRename. Same meaning.
organization.idorganization.deploymentRename — deployment always held the same value.
—contextNew: the course ID on its own, matching the storage URLs and used by provisioning. course still carries the display text.
—location.extidNew: your own ID for the launched destination.

Corrections to these docs​

Behavior that didn't change, but that earlier pages described wrongly:

  • iss is 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 showed link.voshi.com.
  • A launch's location values 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 off location.extid and your own data.

Migration checklist​

  1. Replace user.role reads with user.groups membership checks; store groups against user.member.
  2. Rename resource_link_id → resource_link and organization.id → organization.deployment.
  3. Build and register your Locations URL — without it your tool can't be placed.
  4. Build and register your Provision URL, and call the provision URLs when setup finishes.
  5. If you relied on Voshi copying course data across terms, move that into your provisioning handler.
  6. If you pinned iss to the LMS issuer, pin it to https://api.link.voshi.com instead.