Overview
Webhooks allow your application to receive real-time HTTP notifications when events occur, such as when an evaluation completes. Instead of polling the API, you register a URL and we'll send events to it.
Event Types
Webhook endpoint creation validates event names against the backend enum, but not every enum value is emitted by the current runtime. Agents should only wait for the events in this first table.
| Event | When fired |
|---|---|
evaluation.completed | A submitted evaluation finished grading |
evaluation.failed | An evaluation failed to process |
course.deployed | A course generation request was completed and the course is ready |
course.completed | A course generation request failed in the current backend compatibility path |
slide.completed | A slide deck generation request was completed |
video.completed | A video scene-generation request was completed. Render is not started by create. |
test.deployed | A test generation request was completed |
test.completed | A test generation request failed in the current backend compatibility path |
lti.launch_completed | An LTI launch callback was validated and stored |
lti.grade_synced | An LTI grade-sync job successfully posted a score to the LMS |
lti.grade_sync_failed | An LTI grade-sync job failed after all retries |
Agent rule: for courses and tests, treat course.deployed and test.deployed
as the success events. Treat course.completed and test.completed as
legacy failure-only compatibility events, despite their names.
These values are accepted by the webhook subscription enum but are reserved or not dispatched by the current backend. Do not block an agent workflow waiting for them:
| Event | Current behavior |
|---|---|
assessment.deployed | Reserved for future assessment deployment events |
assessment.submitted | Reserved for future assessment submission events |
slide.deployed | Reserved; slide generation currently emits slide.completed |
video.deployed | Reserved; video generation currently emits video.completed |
lti.launch_failed | Reserved; current launch errors return HTTP errors and do not emit a webhook |
When to Use Webhooks vs Polling
Pick one strategy per resource:
| Strategy | Best for |
|---|---|
| Webhooks | Async generation and server-side integrations with a stable inbound HTTPS endpoint. |
Polling (GET /v1/platform/{resource}/:id) | Sync mode requests. The response already returns the completed result. Polling is only useful if you set mode: "async". |
For autonomous agents with no inbound HTTP, polling is simpler. For server-side integrations, webhooks remove the polling overhead.
Video rendering is currently tracked by polling GET /v1/platform/videos/edit/:editToken
after calling POST /v1/platform/videos/edit/:editToken/render. The Remotion
callback updates renderStatus and videoKey, but it does not emit a separate
public webhook event.
Webhook Payload Shapes
Every webhook delivery sends a JSON body specific to the event type. The
X-Platform-Event header tells you which event you received. Branch on it.
evaluation.completed
{
"evaluationId": "2f4ad455-1e8e-4b6c-9f3a-7ebf4cf6f483",
"status": "COMPLETED",
"evaluationType": "open_ended",
"score": 8,
"maxScore": 10,
"normalizedScore": 80
}evaluation.failed
{
"evaluationId": "2f4ad455-1e8e-4b6c-9f3a-7ebf4cf6f483",
"status": "FAILED",
"error": "Evaluation failed"
}Synchronous failures may also include evaluationType; async job failures only
guarantee evaluationId, status, and error.
course.deployed
{
"courseRequestId": "uuid",
"courseId": "uuid",
"status": "COMPLETED",
"title": "Python for Beginners",
"previewUrl": "https://tutorflow.io/en/platform/courses/edit/{editToken}",
"publicUrl": "https://tutorflow.io/en/platform/courses/{shareToken}/lessons/1"
}course.completed (failure compatibility event)
{
"courseRequestId": "uuid",
"status": "FAILED",
"error": "Course generation failed"
}slide.completed
{
"slideRequestId": "uuid",
"platformSlideId": "uuid",
"status": "COMPLETED",
"previewUrl": "https://tutorflow.io/en/platform/slides/edit/{editToken}"
}video.completed
Fired when scene generation (script + TTS + clip search) completes. The mp4
does not exist yet. Call POST /v1/platform/videos/edit/:editToken/render,
then poll GET /v1/platform/videos/edit/:editToken until renderStatus is
COMPLETED and videoKey is populated.
{
"videoRequestId": "uuid",
"videoId": "uuid",
"status": "COMPLETED",
"previewUrl": "https://tutorflow.io/en/platform/videos/edit/{editToken}"
}test.deployed
{
"testRequestId": "uuid",
"classroomTestId": "uuid",
"status": "COMPLETED",
"title": "Basic Algebra Quiz",
"previewUrl": "https://tutorflow.io/en/platform/tests/{shareToken}",
"publicUrl": "https://tutorflow.io/en/platform/tests/take/{shareToken}"
}test.completed (failure compatibility event)
{
"testRequestId": "uuid",
"status": "FAILED",
"error": "Test generation failed"
}lti.launch_completed
{
"sessionId": "lti-launch-session-uuid",
"messageType": "LtiResourceLinkRequest",
"deploymentId": "lms-deployment-id",
"resourceLinkId": "lms-resource-link-id",
"roles": ["http://purl.imsglobal.org/vocab/lis/v2/membership#Learner"],
"contextId": "lms-course-context-id"
}lti.grade_synced
{
"event": "lti.grade_synced",
"sessionId": "lti-launch-session-uuid",
"resourceLinkId": "lms-resource-link-id"
}lti.grade_sync_failed
{
"event": "lti.grade_sync_failed",
"sessionId": "lti-launch-session-uuid",
"error": "AGS score sync failed with 401: invalid token"
}Signature Verification
Every webhook request includes these headers:
X-Platform-SignatureX-Platform-Event
The signature is computed as HMAC_SHA256(payload, secret) where secret is the
value returned when you created the webhook endpoint. Verify against the raw
request body.
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Retry Policy
If your endpoint returns a non-2xx status code, we retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 25 seconds |
After the initial delivery attempt plus 2 retries (3 total attempts), the delivery is marked as failed.
Managing Webhook Endpoints
Webhook endpoint creation, listing, deletion, and secret rotation are internal dashboard/admin operations in the current MVP. They are intentionally not documented as public agent API surface because Platform API keys cannot call them.
Agents should treat webhook delivery as an operator-configured channel. The agent-facing responsibilities are to store the endpoint secret securely, verify signatures, handle retries idempotently, and process the event payloads listed above.
Best Practices
- Always verify the HMAC signature before processing.
- Return
200 OKquickly and process the event asynchronously. - Handle duplicate events idempotently using the payload resource identifier.
- Use HTTPS endpoints only.