Skip to content

feat: add webhook endpoint for Open edX course staff enrollment#3372

Draft
Anas12091101 wants to merge 3 commits intomainfrom
anas/implement-openedx-course-staff-webhook
Draft

feat: add webhook endpoint for Open edX course staff enrollment#3372
Anas12091101 wants to merge 3 commits intomainfrom
anas/implement-openedx-course-staff-webhook

Conversation

@Anas12091101
Copy link
Contributor

What are the relevant tickets?

https://github.com/mitodl/hq/issues/1209

Description (What does it do?)

This PR adds a webhook endpoint (/api/openedx_webhook/course_staff/) that receives course staff addition notifications from Open edX. When an instructor or staff member is added to a course in Open edX, the plugin POSTs the event to MITx Online, which then automatically enrolls the user as an auditor in the corresponding course run. The endpoint authenticates requests using a shared Bearer token (OPENEDX_WEBHOOK_KEY) and handles user/course lookup, duplicate enrollment checks, and appropriate error responses. This enables seamless synchronization of course staff access between Open edX and MITx Online

Screenshots (if appropriate):

  • Desktop screenshots
  • Mobile width screenshots

How can this be tested?

Additional Context

@Anas12091101 Anas12091101 marked this pull request as draft March 10, 2026 19:24
@github-actions
Copy link

OpenAPI Changes

Show/hide ## Changes for v0.yaml:
## Changes for v0.yaml:
22 changes: 0 error, 21 warning, 1 info
warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/course_certificates/{cert_uuid}/
		removed the optional property 'course_run/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/courses/
		removed the optional property 'results/items/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/courses/{id}/
		removed the optional property 'programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/enrollments/
		removed the optional property '/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API POST /api/v2/enrollments/
		removed the optional property 'run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '201' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.coursepage
		removed the optional property 'items/items/course_details/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.programpage
		removed the optional property 'items/items/program_details/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/pages/{id}/
		removed the optional property '/oneOf[#/components/schemas/CoursePageItem]/course_details/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/pages/{id}/
		removed the optional property '/oneOf[#/components/schemas/ProgramPageItem]/program_details/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_certificates/{cert_uuid}/
		removed the optional property 'program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_enrollments/
		removed the optional property '/items/enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_enrollments/
		removed the optional property '/items/program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		removed the optional property '/items/enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		removed the optional property '/items/program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		removed the optional property 'enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		removed the optional property 'program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/programs/
		removed the optional property 'results/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v2/programs/{id}/
		removed the optional property 'display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API POST /api/v2/verified_program_enrollments/{program_id}/{courserun_id}/
		removed the optional property 'run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '201' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v3/program_enrollments/
		removed the optional property '/items/program/allOf[#/components/schemas/V3SimpleProgram]/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v0.yaml	
	in API GET /api/v3/program_enrollments/{program_id}/
		removed the optional property 'program/allOf[#/components/schemas/V3SimpleProgram]/display_mode' from the response with the '200' status

info	[api-schema-removed] 	
	in components/schemas
		removed the schema 'DisplayModeEnum'



## Changes for v1.yaml:
22 changes: 0 error, 21 warning, 1 info
warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/course_certificates/{cert_uuid}/
		removed the optional property 'course_run/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/courses/
		removed the optional property 'results/items/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/courses/{id}/
		removed the optional property 'programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/enrollments/
		removed the optional property '/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API POST /api/v2/enrollments/
		removed the optional property 'run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '201' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.coursepage
		removed the optional property 'items/items/course_details/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.programpage
		removed the optional property 'items/items/program_details/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/pages/{id}/
		removed the optional property '/oneOf[#/components/schemas/CoursePageItem]/course_details/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/pages/{id}/
		removed the optional property '/oneOf[#/components/schemas/ProgramPageItem]/program_details/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_certificates/{cert_uuid}/
		removed the optional property 'program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_enrollments/
		removed the optional property '/items/enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_enrollments/
		removed the optional property '/items/program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		removed the optional property '/items/enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		removed the optional property '/items/program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		removed the optional property 'enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		removed the optional property 'program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/programs/
		removed the optional property 'results/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v2/programs/{id}/
		removed the optional property 'display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API POST /api/v2/verified_program_enrollments/{program_id}/{courserun_id}/
		removed the optional property 'run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '201' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v3/program_enrollments/
		removed the optional property '/items/program/allOf[#/components/schemas/V3SimpleProgram]/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v1.yaml	
	in API GET /api/v3/program_enrollments/{program_id}/
		removed the optional property 'program/allOf[#/components/schemas/V3SimpleProgram]/display_mode' from the response with the '200' status

info	[api-schema-removed] 	
	in components/schemas
		removed the schema 'DisplayModeEnum'



## Changes for v2.yaml:
22 changes: 0 error, 21 warning, 1 info
warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/course_certificates/{cert_uuid}/
		removed the optional property 'course_run/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/courses/
		removed the optional property 'results/items/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/courses/{id}/
		removed the optional property 'programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/enrollments/
		removed the optional property '/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API POST /api/v2/enrollments/
		removed the optional property 'run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '201' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.coursepage
		removed the optional property 'items/items/course_details/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/pages/?fields=*&type=cms.programpage
		removed the optional property 'items/items/program_details/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/pages/{id}/
		removed the optional property '/oneOf[#/components/schemas/CoursePageItem]/course_details/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/pages/{id}/
		removed the optional property '/oneOf[#/components/schemas/ProgramPageItem]/program_details/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_certificates/{cert_uuid}/
		removed the optional property 'program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_enrollments/
		removed the optional property '/items/enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_enrollments/
		removed the optional property '/items/program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		removed the optional property '/items/enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API DELETE /api/v2/program_enrollments/{id}/
		removed the optional property '/items/program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		removed the optional property 'enrollments/items/run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/program_enrollments/{id}/
		removed the optional property 'program/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/programs/
		removed the optional property 'results/items/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v2/programs/{id}/
		removed the optional property 'display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API POST /api/v2/verified_program_enrollments/{program_id}/{courserun_id}/
		removed the optional property 'run/allOf[#/components/schemas/V2CourseRunWithCourse]/course/allOf[#/components/schemas/V2Course]/programs/items/display_mode' from the response with the '201' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v3/program_enrollments/
		removed the optional property '/items/program/allOf[#/components/schemas/V3SimpleProgram]/display_mode' from the response with the '200' status

warning	[response-optional-property-removed] at head/openapi/specs/v2.yaml	
	in API GET /api/v3/program_enrollments/{program_id}/
		removed the optional property 'program/allOf[#/components/schemas/V3SimpleProgram]/display_mode' from the response with the '200' status

info	[api-schema-removed] 	
	in components/schemas
		removed the schema 'DisplayModeEnum'



Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

openedx/views.py Outdated
Comment on lines +119 to +123
enrollments, edx_request_success = create_run_enrollments(
user,
[course_run],
keep_failed_enrollments=True,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The webhook endpoint returns a success response even when the Open edX enrollment API call fails, because it ignores the edx_request_success flag.
Severity: HIGH

Suggested Fix

The webhook view should check the edx_request_success boolean returned from create_run_enrollments. If edx_request_success is False, the view should return an appropriate error response (e.g., HTTP 500 or 400) instead of an HTTP 200 success response. This will signal to the caller that the end-to-end operation failed and a retry may be needed.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: openedx/views.py#L119-L123

Potential issue: The webhook endpoint in `openedx/views.py` calls
`create_run_enrollments` and captures both the created `enrollments` and the
`edx_request_success` status flag. However, the code only checks if `enrollments` is
truthy before returning an HTTP 200 "Enrollment successful" response. It never inspects
the `edx_request_success` flag. This means if the external Open edX API call fails, a
local enrollment record is still created (with `edx_enrolled=False`), but the webhook
incorrectly reports success. This leads to data inconsistency, as the calling system
believes the user is fully enrolled when they are not.

Did we get this right? 👍 / 👎 to inform future reviews.

@pdpinch
Copy link
Member

pdpinch commented Mar 10, 2026 via email

@Anas12091101 Anas12091101 force-pushed the anas/implement-openedx-course-staff-webhook branch from e7a2816 to c1a9944 Compare March 11, 2026 07:59
Comment on lines +527 to +531
{enrollment.run.start_date
? `Starts ${formatPrettyMonthDate(
parseDateString(enrollment.run.start_date)
)}`
: "Coming Soon"}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enrollment in a course run with a null start date was breaking the dashboard page.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants