Pseudo LTI flow
The Pseudo LTI flow is an API integration flow for platforms that want an LTI-like WeVideo integration but don't support LTI themselves. It mimics the deep linking and grade passback parts of the LTI 1.3 specification, but as a simplified set of JSON API calls with fewer properties and configurable settings.
Throughout this guide, platform means your product — the third-party system, owned by you, that embeds WeVideo.
The pseudo LTI demo is a dummy LMS that walks through the flows using your own API credentials. Its settings let you switch between the callback and postMessage linking modes, between opening content by target link or by content reference, and between the two learner session modes, so you can try each variant before building it. Every launch call, callback and content selected message is shown in an event log, including live signature verification. All demo data stays in your browser — nothing is stored on our side.
How it works
The whole integration consists of:
- One API call from your servers to WeVideo — the launch call, which sets up a session and returns a launch URL.
- Callback calls from WeVideo's servers to yours — the deep linking callback when a teacher selects content, and the grade callback when a learner completes it.
- A client side that embeds WeVideo in an iframe, pointing at the launch URL.
All API calls are server-to-server and all payloads are JSON. The launch call is authenticated like any other WeVideo API call — see Authentication.
The callback calls to your servers are signed with your own API credentials, so you can verify that they actually come from WeVideo. Verifying the signature is optional but recommended — see Verifying callbacks.
There are two launch types: linkingRequest to link a piece of content, and resourceRequest to open it. Linking comes in two variants — one server-to-server, one browser-only — and they hand you different identifiers for the content, which you pass back in the resource launch. Which identifier you send is up to you; WeVideo works out the rest.
The linking flow
A teacher launches WeVideo with launchType: "linkingRequest" to pick a piece of content (an interactive video or playlist) to link into your platform. By default WeVideo reports the selection with a server-to-server call to your deepLinkingCallbackUrl:
Store the targetLink from the deep linking callback — it's what you use to launch the selected content later.
The linking flow without a callback
If you'd rather not implement a callback endpoint, launch with linkingMode: "postMessage". WeVideo then reports the selection as a postMessage to the parent frame instead, and makes no callback call at all:
Store the contentType and contentId from the message — that's what you use to launch the selected content later. Neither deepLinkingCallbackUrl nor deepLinkingReturnUrl is needed in this mode.
The resource flow
A learner opens the linked content with launchType: "resourceRequest", and WeVideo reports the score back when they complete it. Name the content with whichever identifier you stored when it was linked:
- the
targetLinkyou received in the deep linking callback, or - the
contentTypeandcontentIdyou received in the content selected message.
Send one or the other — if you send both, the content reference wins. A target link that points at linked content resolves to the same content type and ID internally, so a platform holding a mix of the two doesn't need to care which it stored.
Content linked through the callback mode already has an assignment — it was created when the teacher made the selection. Content linked through the postMessage mode doesn't, since nothing was called at linking time, so the class and the assignment are created on the first resource launch instead. Either way, later launches reuse them.
The launch call
POST https://www.wevideo.com/api/5/pseudo-lti/launch
This server-to-server call sets up a new session for a user in WeVideo, creating a WeVideo user if necessary. The user is resolved like this:
- WeVideo first looks for an existing user mapped to the provided
userId. - If there's no mapping, it looks for an existing user with the provided
emailaddress. If one is found, it's mapped to theuserId. - Otherwise a new user is created from the
email,firstNameandlastNamevalues, unless there already is another user with the same email address in the WeVideo account.
The typical usage is:
- An end user clicks a button in your UI to launch WeVideo.
- Your client-side code makes a request to your server, which makes the launch call to WeVideo and returns the received
toolLaunchUrlto the client. - The client opens an iframe pointing at the
toolLaunchUrl.
{
"contextId": "course-1234",
"classId": "class-5678",
"className": "Biology 101",
"userId": "platform-user-42",
"firstName": "Ada",
"lastName": "Lovelace",
"email": "ada.lovelace@example.com",
"role": "teacher",
"launchType": "linkingRequest",
"deepLinkingCallbackUrl": "https://platform.example.com/wevideo/deep-linking",
"deepLinkingReturnUrl": "https://platform.example.com/wevideo/close"
}
{
"contextId": "course-1234",
"classId": "class-5678",
"className": "Biology 101",
"userId": "platform-user-97",
"firstName": "Grace",
"lastName": "Hopper",
"email": "grace.hopper@example.com",
"role": "student",
"launchType": "resourceRequest",
"targetLink": "<the targetLink received in the deep linking callback>",
"gradeCallbackUrl": "https://platform.example.com/wevideo/grades"
}
{
"contextId": "course-1234",
"classId": "class-5678",
"className": "Biology 101",
"userId": "platform-user-97",
"firstName": "Grace",
"lastName": "Hopper",
"email": "grace.hopper@example.com",
"role": "student",
"launchType": "resourceRequest",
"contentType": "interactiveVideo",
"contentId": 123456,
"gradeCallbackUrl": "https://platform.example.com/wevideo/grades"
}
The iframe needs allow="microphone; camera; fullscreen" for recording and fullscreen to work. See the embedding guide for details.
Opening the toolLaunchUrl signs the browser in to WeVideo as the launched user. If you test from a browser where you're already logged in to WeVideo, your own session is replaced — use a private window instead, or launch learners with a tab-scoped session, which leaves your own session alone.
Payload properties
| Property | Required | Description |
|---|---|---|
contextId | Yes | A unique identifier for the context WeVideo is opened in. This could be the same as the classId, or it could be something else. |
classId | Yes | A unique identifier for the class (or similar concept) WeVideo is opened from on the platform side. It's included in the callback calls, letting you map each callback to the correct class. |
className | Yes | The name of the class, course or context WeVideo is opened from. It's used to create a corresponding class in WeVideo when a learner opens a linked piece of content. |
userId | Yes | A unique identifier representing the user on the platform side. It's included in the callback calls, letting you map each callback to the correct user. |
firstName | No | Used as the first name if a new WeVideo user needs to be created. Defaults to Firstname if not provided. |
lastName | No | Used as the last name if a new WeVideo user needs to be created. Defaults to Lastname if not provided. |
email | Yes | Used as the email address if a new WeVideo user needs to be created. |
role | Yes | The role the user should have on the WeVideo side. Allowed values are student, teacher and admin. The call never "demotes" a user to a lower-level role, e.g. from admin to student. |
launchType | Yes | linkingRequest to link a new piece of content to your platform, resourceRequest to open linked content. Only teacher and admin level roles can go through a linkingRequest launch. |
linkingMode | No | Only used for linkingRequest launches. callback (the default) reports the selected content with the deep linking callback call; postMessage reports it as a message to the parent frame instead, and makes no callback call. |
sessionAuthMode | No | Only used for learner launches. cookie (the default) signs the browser in to WeVideo with a session cookie; sessionKey scopes the session to the launched tab instead — see Learner sessions. Rejected for teacher and admin launches. |
targetLink | No | For resourceRequest launches: the targetLink value sent to your platform in the deep linking callback. One of the two ways to name the content to open. |
contentType | No | For resourceRequest launches: the type of content to open, interactiveVideo or playlist, as sent in the content selected message. The other way to name the content — set it together with contentId. |
contentId | No | For resourceRequest launches: the WeVideo ID of the content to open, as sent in the content selected message. Set it together with contentType. |
completionType | No | Only used for resourceRequest launches of an interactive video: regular or broadcast. Pass back the value from the content selected message so learners get the experience the teacher picked. Read from the targetLink when you name the content that way. Leaving it out keeps whatever experience the assignment already has. |
dueDate | No | Only used for resourceRequest launches. An optional due date for the assignment, as an ISO 8601 string. Learners can't complete interactions or progress in the attempt after this date. |
gradeCallbackUrl | No | Should only be set for resourceRequest launches. The URL WeVideo makes the grade callback call to when a learner completes an assignment. |
deepLinkingCallbackUrl | No | Should only be set for linkingRequest launches with the default callback linking mode. The URL WeVideo makes the deep linking callback call to when a teacher selects a piece of content. |
deepLinkingReturnUrl | No | Should only be set for linkingRequest launches with the default callback linking mode. The URL the teacher's browser is sent to when they select a piece of content — via an auto-submitted form POST, so the endpoint must accept POST requests. Typically just a URL that closes the WeVideo iframe on the platform side. |
Launch requests with an unrecognized role, launchType, linkingMode, sessionAuthMode, contentType or completionType value are rejected with a 400 response, as are launches that set only one of contentType and contentId, and launches that ask for sessionAuthMode: "sessionKey" for anyone but a learner. A resourceRequest launch that names no content at all just opens WeVideo without opening anything in particular.
Query parameters in gradeCallbackUrl and deepLinkingCallbackUrl are preserved, so you can encode your own identifiers into them — for example https://platform.example.com/wevideo/grades?assignmentId=42.
Response properties
{
"toolLaunchUrl": "https://www.wevideo.com/api/5/pseudo-lti/login/<one-time-token>",
"wevideoUserId": 123456
}
| Property | Description |
|---|---|
toolLaunchUrl | The URL the end user should launch WeVideo from — typically used as the src attribute of an iframe on the platform side. It contains a one-time-use token that logs the user in to WeVideo and opens the app in the right context based on the launch request. |
wevideoUserId | The unique identifier of the user on the WeVideo side. |
Learner sessions
By default, opening the toolLaunchUrl signs the browser in to WeVideo with a session cookie, the same way logging in to WeVideo directly does. That session belongs to the whole browser: the launched user stays signed in if they open WeVideo in another tab, and only one user can be signed in at a time.
Learner launches can ask for a session scoped to the launched tab instead:
{
"contextId": "course-1234",
"classId": "class-5678",
"className": "Biology 101",
"userId": "platform-user-97",
"firstName": "Grace",
"lastName": "Hopper",
"email": "grace.hopper@example.com",
"role": "student",
"launchType": "resourceRequest",
"sessionAuthMode": "sessionKey",
"contentType": "interactiveVideo",
"contentId": 123456,
"gradeCallbackUrl": "https://platform.example.com/wevideo/grades"
}
With sessionAuthMode: "sessionKey", WeVideo hands the browser a session key that it keeps in the launched tab and sends with each of its own API calls, and no usable WeVideo session cookie is left behind. Nothing changes in how you make the launch call, in the toolLaunchUrl you embed, or in the callbacks you receive. Two things change for the end user:
- The session stays in the tab. Opening WeVideo in another tab or window shows the learner as logged out — the launch no longer signs them in to the rest of WeVideo in that browser.
- Different users can be launched at the same time. Because each tab holds its own session, you can have a learner in one tab and a teacher or admin in another, in the same browser.
Constraints
- Learner launches only.
sessionAuthMode: "sessionKey"is rejected with a400unlessroleisstudent. The teacher and admin flows deliberately hand the session from the iframe to a new tab, which a tab-scoped session can't do. - The session ends with the tab. It survives reloads, but closing the tab ends it, and a new tab needs a new launch. Sessions also expire 24 hours after the launch and can't be extended, so a learner still in the same tab after that needs a fresh launch.
The deep linking callback
When a teacher in a linkingRequest launch selects a piece of content, WeVideo makes an API call to the deepLinkingCallbackUrl provided in the launch call. This is the default way a selection is reported — launches with linkingMode: "postMessage" get a message to the parent frame instead, and no callback call is made.
{
"contextId": "course-1234",
"classId": "class-5678",
"userId": "platform-user-42",
"type": "bulb",
"title": "My interactive video",
"targetLink": "<launch link for the selected content>"
}
Payload properties
| Property | Description |
|---|---|
contextId | The context ID received from your platform in the launch call. |
classId | The class ID received from your platform in the launch call. |
userId | The user ID received from your platform in the launch call. |
type | The type of content that was selected. Either bulb (interactive video) or playlist. |
title | The title of the selected content, e.g. My interactive video. |
targetLink | The link back to WeVideo that launches the selected piece of content. This is the targetLink value your platform should provide in the launch call for a resource launch. |
The content selected message
When a teacher in a linkingRequest launch with linkingMode: "postMessage" selects a piece of content, the WeVideo iframe posts a message to its parent frame — your page — instead of WeVideo calling your server. There's no callback call and no form POST to a return URL in this mode, so your page owns both storing the selection and closing the iframe.
Linking is a single click in this mode: picking content from the list reports it immediately, with no confirmation step in between. The message therefore only names the content — see the note below if you need a learner experience or a due date.
{
type: "wevideo:contentSelected",
contentType: "interactiveVideo",
contentId: 123456,
title: "My interactive video"
}
Message properties
| Property | Description |
|---|---|
type | Always the literal string wevideo:contentSelected. Check it before handling a message — your window receives messages from other sources too. |
contentType | The type of content that was selected, interactiveVideo or playlist. Pass it back as contentType in the resource launch. |
contentId | The WeVideo ID of the selected content, as a number. Pass it back as contentId in the resource launch. |
title | The title of the selected content, e.g. My interactive video. Omitted if the content has no title. |
Store the message and pass the values back in the launch call when a learner opens the content:
window.addEventListener("message", event => {
// The iframe is served from your WeVideo host — check the origin before trusting the message
if (event.origin !== "https://www.wevideo.com") return;
if (event.data?.type !== "wevideo:contentSelected") return;
saveLinkedContent({
contentType: event.data.contentType,
contentId: event.data.contentId,
title: event.data.title,
});
closeWeVideoIframe();
});
The message is posted without a fixed target origin, since WeVideo doesn't know your page's origin. Always verify event.origin against the WeVideo host you launched, and verify event.data.type, before acting on a message.
Choosing a learner experience or due date
The callback linking mode shows the teacher a confirmation step where they can pick the learner experience (self-paced or live broadcast) and a due date. The postMessage mode has no such step — one click links the content — so neither is chosen in WeVideo.
If you need them, set them from your own side: completionType and dueDate on the resource launch apply to the assignment, so you can collect them in your own UI and pass them along with every launch. completionType defaults to regular for content that has no assignment yet, and leaving it out never changes an assignment that already exists.
The grade callback
When a learner in a resourceRequest launch completes an interactive video or playlist, WeVideo makes an API call to the gradeCallbackUrl provided in the launch call.
{
"contextId": "course-1234",
"classId": "class-5678",
"userId": "platform-user-97",
"timestamp": "2026-07-22T14:03:22.123Z",
"scoringUserId": null,
"scoreGiven": 8.0,
"scoreMaximum": 10.0,
"targetLink": "<the targetLink of the completed resource>",
"contentType": null,
"contentId": null
}
Payload properties
| Property | Description |
|---|---|
contextId | The context ID the resource attempt was completed in. |
classId | The class ID of the resource attempt. |
userId | The user ID that completed the attempt. |
timestamp | The time the interactive video or playlist attempt was completed, as an ISO 8601 string. |
scoringUserId | Normally null. If a teacher, for example, adjusts the score for an assignment, this is the user ID of that teacher. |
scoreGiven | The score the user received, as a numeric value. |
scoreMaximum | The total available score, as a numeric value. |
targetLink | The targetLink of the completed resource, as provided in the launch call. Lets you tell which resource the grade belongs to when several pieces of content are linked to the same class. null if the launch didn't provide one. |
contentType | The type of the completed resource, interactiveVideo or playlist. Set whenever WeVideo knows it — either because you sent it, or because it resolved it from your targetLink. |
contentId | The WeVideo ID of the completed resource, on the same terms as contentType. |
Responding to callbacks
Your callback endpoints should respond with a 2xx status code. The response body is ignored.
- The deep linking callback is made while the teacher waits: it must succeed before the teacher is redirected to the
deepLinkingReturnUrl, so a non-2xxresponse fails the content selection. Keep the handler fast and defer any heavy processing. - The grade callback is made once per completed attempt and is not retried. If your endpoint responds with a non-
2xxstatus, the score report is lost (the failure is logged on the WeVideo side).
Verifying callbacks
The callback calls are signed with the same HMAC scheme you use to authenticate your calls to the WeVideo API, using your own API key and secret. Each callback request includes these headers:
| Header | Value |
|---|---|
Authorization | WEV <your API key>:<signature> |
Date | The time the request was signed |
The signature is computed like this:
StringToSign = "POST" + "\n" + MD5(RequestBody) + "\n" + Date + "\n" + CallbackUrl
Signature = Base64(HMAC-SHA256(YourAPISecret, UTF-8(StringToSign)))
Where CallbackUrl is the full callback URL exactly as you provided it in the launch call (including any query string), MD5(RequestBody) is the lowercase hex MD5 of the raw request body, and Date is the value of the Date header as-is.
Keep — or deterministically rebuild — the exact callback URL string you sent in the launch call; a single differing character (encoding, parameter order, a trailing slash) makes verification fail.
To verify a callback, recompute the signature and compare it to the one in the Authorization header:
import base64
import hashlib
import hmac
def verify_callback(headers, raw_body: bytes, callback_url: str, api_key: str, api_secret: str) -> bool:
authorization = headers.get("Authorization", "")
if not authorization.startswith("WEV "):
return False
key, _, signature = authorization[4:].partition(":")
if key != api_key:
return False
body_md5 = hashlib.md5(raw_body).hexdigest()
string_to_sign = f"POST\n{body_md5}\n{headers['Date']}\n{callback_url}"
expected = base64.b64encode(
hmac.new(api_secret.encode(), string_to_sign.encode(), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(signature, expected)
If the signature is missing or doesn't verify, treat the callback as untrusted. You can also reject requests whose Date header is more than a few minutes old to guard against replays.