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 both flows using your own API credentials. Every launch call and callback 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.
- Two 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, forming two flows.
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:
Store the targetLink from the deep linking callback — it's what you use to launch the selected content later.
The resource flow
A learner opens the linked content with launchType: "resourceRequest". When they complete it, WeVideo reports the score back:
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"
}
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.
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 if the launch is intended to link a new piece of content to your platform, resourceRequest if not. Only teacher and admin level roles can go through a linkingRequest launch. |
targetLink | No | The internal link the user is redirected to after login. Should only be set for resourceRequest launches, and should be the targetLink value sent to your platform in the deep linking callback. |
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. 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. 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 or launchType value are rejected with a 400 response.
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. |
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.
{
"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 resourceRequest launch. |
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>"
}
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. |
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.