Woolf API Overview
The Woolf API is a GraphQL-based interface that allows seamless integration with Woolf's Academic Management System (AMS). This reference guide provides detailed information on queries, mutations, schema structure, and how to interact with the API efficiently.
API Authorization
To access the API, you must authenticate using a College Token.
Retrieve your college token from the Configurations tab in College Settings on AMS.
Include the token as a Bearer Token in all API requests.
{ "Authorization": "Bearer REPLACE_WITH_YOUR_COLLEGE_TOKEN" }
GraphQL Schema Overview
The Woolf API follows the GraphQL specification, which is structured into different core elements.
The schema below was generated from the live API on 12 October 2026. You can always get the current schema yourself. These links are generated from the live API, so they always match production, and need no token:
JSON (GraphQL introspection result): https://api.woolf.university/v1/schema.json
SDL (GraphQL schema language): https://api.woolf.university/v1/schema.graphql
You can also explore it in GraphiQL, or send a standard GraphQL introspection query to POST https://api.woolf.university/v1 (sandbox: https://api.x.university/v1).
API Schema
# === Queries === type Query { activities( courseId: String kinds: [ActivityKind!] pagination: Pagination = {skip: 0, take: 100} resourceId: String resourceKinds: [ResourceKind!] statuses: [ActivityStatus!] studentId: String ): ActivityPaginationResult! activity(id: ID!): Activity! college: College! courseStudents( filters: CourseStudentsFilterInput pagination: Pagination = {skip: 0, take: 200} ): CourseStudentPaginationResult! course(id: ID!): Course! courses( degreeId: ID pagination: Pagination = {skip: 0, take: 200} statuses: [CourseStatus!] ): CoursePaginationResult! jobDetails(jobId: String!): JobDetails! myDegreeProgress(degreeId: ID!): StudentDegreeProgressSnapshot! resource(id: String!): Resource! resources( courseId: ID kinds: [ResourceKind!] pagination: Pagination = {skip: 0, take: 200} statuses: [ResourceStatus!] ): ResourcePaginationResult! studentDegreeProgress(degreeId: ID!, id: String!): StudentDegreeProgressSnapshot! student(id: String!): Student! students( courseId: ID degreeId: ID pagination: Pagination = {skip: 0, take: 200} ): StudentPaginationResult! webhooks(pagination: Pagination = {skip: 0, take: 200}): WebhookPaginationResult! } # === Mutations === type Mutation { """ Records a student's attendance at a meeting. The attendance is always stored; `warnings` lists the reasons it will not count toward the student's course requirements yet (RESOURCE_NOT_VERIFIED, RESOURCE_HAS_NO_WORKLOAD). """ addAttendance(attendance: AttendanceInput!): ActivityOutput! addFeedback(feedback: FeedbackInput!): ActivityOutput! addGrade(grade: GradeInput!): ActivityOutput! """ Cross-lists one of your college's resources to more of your courses, degrees or your whole college, so its workload also counts there. Tags the resource already has are skipped, so the call is safe to repeat. Past completions of the resource that no course could credit yet are retried against the new tags; a completion already allocated to a course stays where it is. """ addResourceCrossListingTags(resourceId: ID!, tags: [CrossListingTagInput!]!): Resource! addResource(courseId: ID!, resource: ResourceCreateInput!): ResourceOutput! addStudentToCollege(email: String!, student: StudentInput): Student! addStudentToDegree( currency: Currency degreeId: ID! email: String! isScholarship: Boolean student: StudentInput tuitionCost: Int ): Student! addSubmission(submission: SubmissionInput!): ActivityOutput! addTeacherToCollege( courseIds: [ID!] email: String! teacher: TeacherInput ): CollegeStaff! addWebhook(events: [WebhookEvent!]!, url: String!): Webhook archiveActivity(activityId: ID!): Boolean archiveCourseProgress(courseId: ID!, studentId: ID!): Boolean archiveDegreeStudent( degreeId: ID degreeStudentId: ID reason: String! studentId: ID ): Boolean! bulkUploadActivity: Json! changeDegreeStudentScholarship( degreeId: ID degreeStudentId: ID isScholarship: Boolean! studentId: ID ): Boolean! changeEmail(email: String!, id: ID!): Boolean! changeWebhook(events: [WebhookEvent!], id: ID!, url: String): Webhook generateUserToken(id: String!): String! modifyResource(id: ID!, resource: ResourceModifyInput!): ResourceOutput! pauseDegreeStudent( degreeId: ID degreeStudentId: ID pauseFrom: Date pauseTo: Date reason: String! studentId: ID ): Boolean! """ Removes cross-listing tags of your college from one of its resources. Tags the resource does not have are ignored. The tag linking a resource to its own course cannot be removed. Removing a tag stops new completions counting toward that course; a completion already provisionally allocated to it is not reallocated. """ removeResourceCrossListingTags( resourceId: ID! tags: [CrossListingTagInput!]! ): Resource! removeWebhook(id: ID!): Boolean submitStudentsForEvaluation(evaluation: EvaluationInput!): [ID] unpauseDegreeStudent(degreeId: ID, degreeStudentId: ID, studentId: ID): Boolean! } # === Input Types === input AssetInput { contentType: String fileName: String importUrl: String } input AttendanceInput { studentId: String! resourceId: String! timestamp: String } input CourseStudentsFilterInput { readyForSubmission: Boolean isActiveDegreeStudent: Boolean courseIds: [ID!] degreeIds: [ID!] statuses: [CourseStudentStatus!] createdGTE: String createdLTE: String """ ISO 8601. Inclusive lower bound on `accomplished`. Always pair with `statuses: [PASSED]`: only there is `accomplished` the pass time. """ accomplishedGTE: String """ ISO 8601. Inclusive upper bound on `accomplished`; a bare date covers that whole day (UTC). """ accomplishedLTE: String """ Woolf user ids — the `studentId` a STUDENT_PASSED_COURSE webhook carries. Limits to those students. """ userIds: [ID!] """ Your college's own student ids (the external id you set on each student). Limits to those students. """ externalIds: [String!] } """ A cross-listing tag in your own college. A COLLEGE tag needs no id: it is always your college. """ input CrossListingTagInput { level: CrossListingTagLevel! """Required for a COURSE tag.""" courseId: ID """Required for a DEGREE tag.""" degreeId: ID } input EvaluationInput { courseId: String! studentIds: [String!]! tierId: ID } input FeedbackInput { activityId: ID resourceId: String studentId: String! teacherId: String content: String assets: [AssetInput!] evidenceAssets: [AssetInput!] } input GradeInput { activityId: ID resourceId: String studentId: String! teacherId: String weightId: ID value: Float! assets: [AssetInput!] evidenceAssets: [AssetInput!] } input Pagination { skip: Int! = 0 """ Limits the number of returned records. To return all records specify `null` """ take: Int = 100 } input ResourceCreateInput { name: String! kind: ResourceKind! isGradeRequired: Boolean = true cohortName: String externalId: String content: String workload: Int weightIds: [ID!] assets: [AssetInput!] tags: [String!] cohortNames: [String!] teacherIds: [ID!] } input ResourceModifyInput { name: String kind: ResourceKind isGradeRequired: Boolean cohortName: String externalId: String content: String workload: Int weightIds: [ID!] assets: [AssetInput!] tags: [String!] cohortNames: [String!] teacherIds: [ID!] } input StudentInput { externalId: String nameFirst: String nameLast: String } input SubmissionInput { activityId: ID resourceId: String studentId: String content: String assets: [AssetInput!] evidenceAssets: [AssetInput!] } input TeacherInput { externalId: String } # === Enums === enum ActivityKind { addFeedback addGrade submitAssignment consumeResource attendMeeting } enum ActivityStatus { ACTIVE ARCHIVED PENDING REJECTED } enum CollegeStatus { DRAFT VERIFIED REJECTED ARCHIVED TEACH_OUT } enum ComplianceStepStatus { PENDING REJECTED SUBMITTED COMPLETED } enum CourseStatus { DRAFT REJECTED VERIFIED ARCHIVED } enum CourseStudentStatus { ACTIVE SUBMITTED PASSED FAILED } """ The levels a college can tag at. UNIVERSITY is reserved for Woolf staff. """ enum CrossListingTagLevel { COLLEGE DEGREE COURSE } enum Currency { USD EUR INR AED GBP PHP UAH THB TND NGN MAD DZD XOF KES } enum DegreeCourseKind { COMPULSORY ELECTIVE } enum DegreeStudentPaymentStatus { UNPAID PAID } enum DegreeStudentStatus { PENDING ACTIVE REJECTED SUBMITTED COMPLETED ARCHIVED PAUSED } enum GradingScheme { NUMERIC PASS_FAIL } enum JobKind { BULK_ACTIVITY_IMPORT } enum JobStatus { PENDING PROCESSING COMPLETED FAILED } enum ResourceKind { ASSIGNMENT ASSIGNMENT_SUMMATIVE MEETING GENERAL PUBLICATION PUBLICATION_REVIEWED } enum ResourceStatus { DRAFT REJECTED SUBMITTED VERIFIED ARCHIVED } enum TierExemptionSource { AIRLOCK_API AIRLOCK_SDK AMS IMPORT IMPORT_RPL RPL_REQUEST STAI_CREDIT UNKNOWN } enum TierKind { TIER SPECIALIZATION } enum WebhookEvent { REJECTED_RESOURCE STUDENT_PASSED_COURSE IMPORTED_ASSETS DEGREE_STUDENT_ACTIVATED DEGREE_STUDENT_COMPLETED_APPLICATION DEGREE_STUDENT_ARCHIVED DEGREE_STUDENT_DELISTED DEGREE_STUDENT_COMPLETED ACTIVITY_UPLOAD_FINISHED } # === Objects === type Activity { id: ID! created: DateTime! updated: DateTime! userId: ID! collegeId: ID! degreeId: ID weightId: ID courseId: ID resourceId: ID data: Json! kind: ActivityKind! status: ActivityStatus! resourceKind: ResourceKind grade: Float assets: [Asset!]! evidenceAssets: [Asset!]! } type ActivityOutput { activityId: String! assets: [AssetUpload!]! evidenceAssets: [AssetUpload!]! """ Non-fatal problems with the recorded activity, e.g. attendance on a meeting that is not VERIFIED and so does not count yet. Only mutations whose description mentions `warnings` populate it; on the others it is always empty and says nothing about whether the activity counts. """ warnings: [ActivityWarning!]! } type ActivityPaginationResult { count: Int! list: [Activity]! } type ActivityWarning { """ Stable machine-readable code: RESOURCE_NOT_VERIFIED or RESOURCE_HAS_NO_WORKLOAD. New codes may be added; treat unknown codes as informational. """ code: String! """ Human-readable explanation for logs and support. Informational only: the wording may change, so branch on `code`, not on this text. """ message: String! } type Asset { id: ID! fileName: String! contentType: String downloadUrl: String updated: DateTime! created: DateTime! } type AssetUpload { headers: Json uploadUrl: String } type College { id: ID! name: String! descr: Json deanId: ID deanName: String deanEmail: String status: CollegeStatus! rejectionReason: String updated: DateTime! created: DateTime! degrees: [Degree!]! teachers: [CollegeStaff!]! } type CollegeStaff { id: ID! externalId: String created: DateTime! updated: DateTime! name: String! email: String! } type Course { id: ID! updated: DateTime! created: DateTime! status: CourseStatus! rejectionReason: String name: String! descr: String! weights: [Weight!]! workloadRequired: Int! workload: Int! publicationRequired: Int! publicationReviewedRequired: Int! meetingRequired: Int! assignmentRequired: Int! assignmentSummativeRequired: Int! } type CoursePaginationResult { count: Int! list: [Course]! } type CourseStudent { id: ID! userId: ID! updated: DateTime! created: DateTime! """ The pass time on PASSED rows. Only meaningful there: an ACTIVE or SUBMITTED row can still hold the time its last submission was rejected. """ accomplished: DateTime courseId: ID! rejectionReason: String runningAverage: Float! finalAverage: Float! degreeId: ID status: CourseStudentStatus workloadRequired: Int! workloadCompleted: Int! meetingRequired: Int! meetingCompleted: Int! publicationReviewedCompleted: Int! publicationReviewedRequired: Int! assignmentRequired: Int! assignmentCompleted: Int! assignmentSummativeRequired: Int! assignmentSummativeCompleted: Int! } type CourseStudentPaginationResult { count: Int! list: [CourseStudent]! } type CrossListingTag { id: ID! level: String! collegeId: ID collegeName: String degreeId: ID degreeName: String courseId: ID courseName: String order: Int } type Degree { id: ID! updated: DateTime! created: DateTime! rejectionReason: String name: String! descr: Json courses: [Course!]! } type DegreeStudent { id: ID! degreeId: ID! updated: DateTime! created: DateTime! status: DegreeStudentStatus identityStatus: ComplianceStepStatus! qualificationStatus: ComplianceStepStatus! motivationStatus: ComplianceStepStatus! agreementStatus: ComplianceStepStatus! approvalStatus: ComplianceStepStatus! workloadConsumed: Int! workloadRequired: Int! workloadCompleted: Int! onboardingStarted: DateTime identityTimestamp: DateTime qualificationTimestamp: DateTime motivationTimestamp: DateTime agreementTimestamp: DateTime approvalTimestamp: DateTime paymentTimestamp: DateTime paymentStatus: DegreeStudentPaymentStatus! activated: DateTime rejectionReason: String } type JobDetails { id: String! status: JobStatus! kind: JobKind fileName: String fileKind: String url: JobFileInfo resultsUrl: JobFileInfo assets: [Asset!]! created: DateTime! updated: DateTime! successCount: Int errorCount: Int } type JobFileInfo { path: String downloadUrl: String headers: Json } type Resource { id: ID! externalId: String created: DateTime! updated: DateTime! courseId: ID cohortId: ID isV2: Boolean! crossListingTags: [CrossListingTag!]! cohortName: String cohortNames: [String!] rejectionReason: String name: String! content: String workload: Int! workloadMultiplier: Float! workloadExecution: Int! kind: ResourceKind! status: ResourceStatus! weights: [Weight!] assets: [Asset!]! } type ResourceOutput { resource: Resource! assets: [AssetUpload!]! } type ResourcePaginationResult { count: Int! list: [Resource]! } type Student { id: ID! externalId: String name: String! email: String! updated: DateTime! created: DateTime! degrees: [DegreeStudent!]! courses: [CourseStudent!]! } type StudentDegreeProgressSnapshot { degree: StudentDegreeProgressSnapshotDegree! degreeId: ID! degreeStudentId: ID! finalAverage: Float gpa: Float status: DegreeStudentStatus! workloadCompletedHours: Float! workloadCompletedMinutes: Int! workloadConsumedHours: Float! workloadConsumedMinutes: Int! workloadInProgressHours: Float! workloadInProgressMinutes: Int! workloadProvisionalHours: Float! workloadProvisionalMinutes: Int! workloadRequiredHours: Float! workloadRequiredMinutes: Int! } type StudentDegreeProgressSnapshotCourse { courseId: String! degreeCourseKind: DegreeCourseKind! finalAverage: Float name: String! provisionalWorkloadHours: Float! provisionalWorkloadMinutes: Int! provisionalWorkloadSolidHours: Float! provisionalWorkloadSolidMinutes: Int! requirementCounters: StudentDegreeProgressSnapshotCourseRequirementCounters! runningAverage: Float status: CourseStudentStatus workloadConsumedHours: Float! workloadConsumedMinutes: Int! workloadRequiredHours: Float! workloadRequiredMinutes: Int! } type StudentDegreeProgressSnapshotCourseRequirementCounters { accomplished: DateTime consumedResourceAssignmentAmount: Int! consumedResourceAssignmentSummativeAmount: Int! consumedResourceMeetingAmount: Int! consumedResourcePublicationReviewedAmount: Int! gradingScheme: GradingScheme hasDegreeAffiliation: Boolean! isOnboardingCompleted: Boolean! minimumCourseWorkloadHours: Float! minimumCourseWorkloadMinutes: Int! resourceAssignmentAmountRequired: Int! resourceAssignmentSummativeAmountRequired: Int! resourceMeetingAmountRequired: Int! resourcePublicationReviewedAmountRequired: Int! } type StudentDegreeProgressSnapshotDegree { id: ID! name: String! specializations: [StudentDegreeProgressSnapshotTier!]! tiers: [StudentDegreeProgressSnapshotTier!]! workloadRequiredHours: Float workloadRequiredMinutes: Int } type StudentDegreeProgressSnapshotTier { courses: [StudentDegreeProgressSnapshotCourse!]! cumulativeWorkloadConsumedHours: Float! cumulativeWorkloadConsumedMinutes: Int! cumulativeWorkloadProvisionalHours: Float! cumulativeWorkloadProvisionalMinutes: Int! cumulativeWorkloadProvisionalSolidHours: Float! cumulativeWorkloadProvisionalSolidMinutes: Int! id: ID! kind: TierKind! name: String! position: Int! tierProgress: StudentDegreeProgressSnapshotTierProgress tierWorkloadRequiredHours: Float! tierWorkloadRequiredMinutes: Int! } type StudentDegreeProgressSnapshotTierProgress { exemptionGrantedAt: DateTime exemptionSource: TierExemptionSource isExempted: Boolean! workloadCompletedMinutes: Int! workloadCompletedHours: Float! workloadCompulsoryCompletedMinutes: Int! workloadCompulsoryCompletedHours: Float! workloadCompulsoryRequiredMinutes: Int! workloadCompulsoryRequiredHours: Float! workloadElectiveCompletedMinutes: Int! workloadElectiveCompletedHours: Float! workloadElectiveRequiredMinutes: Int! workloadElectiveRequiredHours: Float! workloadRequiredMinutes: Int! workloadRequiredHours: Float! workloadTransferredMinutes: Int! workloadTransferredHours: Float! } type StudentPaginationResult { count: Int! list: [Student]! } type Webhook { id: ID! url: String! events: [WebhookEvent!]! created: String! updated: String! } type WebhookPaginationResult { count: Int! list: [Webhook]! } type Weight { id: ID! created: DateTime! updated: DateTime! courseId: ID! name: String! percentage: Int! quantity: Int! } # === Scalars === """ A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar Date """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. """ scalar DateTime scalar Json
Element | Description |
|---|---|
Queries | Used to fetch data from the API, such as retrieving student records, course details, and activities. |
Mutations | Used to modify data, such as adding students, updating grades, and submitting assignments. |
Objects | Represents structured data returned by queries and mutations |
Input Types | Defines structured objects used as inputs for mutations. |
Enums | Defines a fixed set of values. |
Scalars | Built-in primitive types. |
Queries
Queries in GraphQL are allows fetching and retrieving data from the Woolf API. You can use them to:
Retrieve structured information (e.g., get student details, fetch course lists, check resource data).
Filter and refine results using parameters (e.g., filter students by course, fetch active resources only).
Request specific fields to optimize API responses and reduce data load.
Mutations
Mutations in GraphQL are used to modify or add data in the Woolf API.
Unlike queries, which retrieve data, mutations allow you to:
Create new records (e.g., enroll students, add resources, submit assignments).
Update existing records (e.g., modify student details, change emails, update grades).
Perform actions (e.g., submit feedback, archive progress, generate authentication tokens).
Best Practices for Using the Woolf API
Use the API Sandbox for Testing: Before deploying to production, test your API queries and mutations using the API Sandbox.
Secure API Authentication:
Always include a valid Bearer Token in the
Authorizationheader.
Optimize Queries for Performance:
Request only the fields you need to avoid unnecessary data retrieval.
Use pagination for queries returning large datasets.
Apply filters to narrow down results and reduce response times.
Handle API Errors Gracefully: Check for common error codes and log errors for debugging and notify the Woolf of persistent failures.
401 Unauthorized– Invalid or missing API token.403 Forbidden– Insufficient permissions.400 Bad Request– Incorrect query structure or missing fields.
FAQs
How do I authenticate API requests?
All requests require a valid API token passed in the Authorization header.
{ "Authorization": "Bearer YOUR_COLLEGE_TOKEN" }
To obtain a token, refer to the authentication steps in the Getting Started for Developers.
How do I add a new student using the API?
Use the addStudentToCollege or addStudentToDegree mutations.
How do I handle API rate limits?
The Woolf API may have rate limits. If you encounter a 429 Too Many Requests error:
Reduce request frequency.
Implement exponential backoff for retries.
How do I get help with API integration?
For technical assistance, contact Woolf Support via support@woolf.education.