# The Learning Mate — Public API (full reference) Programmatic access to your courses, exams, students, and grades on The Learning Mate. This file contains the complete v1 API reference. See also the machine-readable OpenAPI spec at https://thelearningmate.com/api/public/v1/openapi.json and the human docs at https://thelearningmate.com/api-docs/reference. ## Overview - Base URL: `https://thelearningmate.com/api/public/v1` - Authentication: send `Authorization: Bearer pk_your_api_key_here` on every request. Keys are issued by our team. - Response envelope: `{ "success": true, "data": ... }` on success; `{ "success": false, "error": { "code", "message" } }` on failure. - Rate limits: an IP throttle of 120 requests/minute applies before authentication, plus a per-API-key limit on each endpoint (noted below). A `429` includes a `Retry-After` header. ### Error codes - `400 VALIDATION_ERROR` / `INVALID_JSON` — the request failed validation or was not valid JSON. - `401 UNAUTHENTICATED` / `INVALID_API_KEY` — the API key was missing, malformed, or invalid. - `404 NOT_FOUND` — the referenced resource does not exist or is not owned by your key. - `429 RATE_LIMIT_EXCEEDED` — too many requests; retry after `Retry-After`. - `5xx INTERNAL_ERROR` / `FINALIZATION_DISPATCH_FAILED` — a server-side error; safe to retry. --- ## Embedding the student experience Prefer not to build against the API? You can embed the full student experience — exam list, answer-sheet upload, AI-marked results, and petitions — directly in your own site with a single ` ``` ### Customization Customize the embed with query parameters on the iframe URL. Invalid values are ignored — they never break the embed. | Parameter | Example | Effect | | --- | --- | --- | | `examId` | `examId=abc123` | Auto-opens this exam's details dialog after sign-in. | | `courseId` | `courseId=xyz` | Shows only this course; falls back to all courses if the student isn't enrolled in it. | | `primaryColor` | `primaryColor=7c3aed` | Accent color for buttons, focus rings, and charts as a 6-digit hex, with or without a leading #. Text contrast is chosen automatically. | | `hideLogo` | `hideLogo=1` | Hides the Learning Mate logo on the sign-in card. | | `theme` | `theme=dark` | light (the default) or dark. | The visual parameters are captured at sign-in and persist for the whole embedded session, so in-iframe navigation and reloads keep your look. ### Sign-in Students sign in with their **email or mobile number and password** — the same Learning Mate student credentials — entered directly in the iframe. Only **student** accounts can sign in through an embed. Embedded sessions last 12 hours and re-authenticate inside the iframe when they expire. ### Auto sign-in (optional) If your site already holds the student's credentials, you can sign them in automatically. Because the session cookie must be created from inside the iframe (so it lands in your site's cookie partition), you hand the credentials to the iframe via `postMessage` rather than calling the API yourself. The iframe posts a `ready` message once it is loaded and signed-out; reply with the credentials: ```html ``` On success the iframe navigates itself to the student area; on failure it posts `{ source: 'pedai-embed', type: 'signin-error', code }` (`INVALID_CREDENTIALS`, `STUDENT_ONLY`, `RATE_LIMITED`, `INTERNAL_ERROR`) and shows the manual form as a fallback. Only send credentials your site legitimately holds, and always target the exact Learning Mate origin (never `'*'`). ### Browsers & cookies Inside an iframe the session cookie is set with `SameSite=None; Secure; Partitioned` ([CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), supported by current Chrome, Edge, Firefox, and Safari 18.4+. The cookie is partitioned to your site — it never leaks into a first-party Learning Mate session. HTTPS is required. On browsers that reject partitioned cookies, sign-in appears to succeed but the next page load returns to the embedded sign-in form; offer an "open in a new tab" link to the same URL as a fallback: ```html Open in a new tab ``` --- ## GET /api/public/v1/courses — List your courses Lists the courses owned by the authenticated API key so you can resolve stable `courseId` values. Pass an optional `name` query parameter to filter by a case-insensitive exact name match. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 60 requests / minute (per API key) **Query parameters** - `name` (string, optional) — Case-insensitive exact course-name filter. **Example request** ```bash curl -X GET "https://thelearningmate.com/api/public/v1/courses?name=Algebra+101" \ -H "Authorization: Bearer pk_your_api_key_here" ``` **Response data fields** - `courses` (array, required) - `courses[].courseId` (string, required) - `courses[].name` (string, required) - `courses[].subject` (string | null, required) - `courses[].language` (string, required) - `courses[].year` (integer, required) - `courses[].educationCenter` (string | null, required) - `courses[].externalIntegration` (string | null, required) **Example response (200)** ```json { "success": true, "data": { "courses": [ { "courseId": "c_9f8a7b6c5d4e", "name": "Algebra 101", "subject": "Mathematics", "language": "en", "year": 2025, "educationCenter": "Central High School", "externalIntegration": null } ] } } ``` --- ## GET /api/public/v1/courses/{courseId}/exams — List exams in a course Lists the exams that belong to a course so you can resolve stable `examId` values. Requires ownership of the course. Pass an optional `name` query parameter to filter by a case-insensitive exact name match. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 60 requests / minute (per API key) **Path parameters** - `courseId` (string, required) **Query parameters** - `name` (string, optional) — Case-insensitive exact exam-name filter. **Example request** ```bash curl -X GET "https://thelearningmate.com/api/public/v1/courses/c_9f8a7b6c5d4e/exams?name=Midterm+Exam" \ -H "Authorization: Bearer pk_your_api_key_here" ``` **Response data fields** - `exams` (array, required) - `exams[].examId` (string, required) - `exams[].courseId` (string, required) - `exams[].name` (string, required) - `exams[].maxGrade` (number, required) - `exams[].isCreating` (boolean, required) - `exams[].isGradesPublished` (boolean, required) - `exams[].isStudentUploadEnabled` (boolean, required) - `exams[].isVisibleToNewStudents` (boolean, required) - `exams[].isMarkSchemeVisible` (boolean, required) - `exams[].gradingMode` (enum(instant | standard | premium), required) - `exams[].examCategory` (enum(Homework | Classwork | Quiz | Assignment | Mock | Midterm | Final | Past Papers) | null, required) - `exams[].examTypeId` (string | null, required) - `exams[].dueDateTime` (string | null, required) - `exams[].createdAt` (string | null, required) - `exams[].updatedAt` (string, required) **Example response (200)** ```json { "success": true, "data": { "exams": [ { "examId": "e_1a2b3c4d5e6f", "courseId": "c_9f8a7b6c5d4e", "name": "Midterm Exam", "maxGrade": 100, "isCreating": false, "isGradesPublished": true, "isStudentUploadEnabled": false, "isVisibleToNewStudents": false, "isMarkSchemeVisible": false, "gradingMode": "standard", "examCategory": "Midterm", "examTypeId": null, "dueDateTime": null, "createdAt": "2026-05-14T09:30:00.000Z", "updatedAt": "2026-05-14T09:30:00.000Z" } ] } } ``` --- ## POST /api/public/v1/courses/{courseId}/exams — Create an exam Creates an exam in a course. The exam is prepared in the background, so this returns `202 Accepted` immediately; poll `GET /api/public/v1/exams/{examId}` until `status` becomes `ready` (or `failed`). Requires ownership of the course. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 5 requests / hour (per API key) **Notes** - Provide the exam paper as `examPaperPdfUrl` — a single PDF URL. - The mark scheme is optional and may be supplied as `markSchemePdfUrl`. - `gradingMode` is one of `instant`, `standard`, or `premium`. **Path parameters** - `courseId` (string, required) **Request body fields** - `examName` (string, required) — Exam display name. - `maxGrade` (number, required) — Maximum attainable grade. - `examPaperPdfUrl` (string, required) — Publicly fetchable exam-paper PDF URL. - `markSchemePdfUrl` (string, optional) — Optional publicly fetchable mark-scheme PDF URL. - `isVisibleToNewStudents` (boolean, optional) — Whether students enrolled later see the exam. Defaults to false. - `initialVisibility` (enum(visible | hidden), optional) — Initial visibility for currently enrolled students. Defaults to visible. - `gradingMode` (enum(instant | standard | premium), optional) — Grading mode. Defaults to standard. - `isStudentUploadEnabled` (boolean, optional) — Whether students may upload answer papers. Defaults to false. - `isGradesPublished` (boolean, optional) — Whether grades are published. Defaults to false. - `isMarkSchemeVisible` (boolean, optional) — Whether students may see the mark scheme. Defaults to false. - `dueDateTime` (string | null, optional) — ISO 8601 due date/time, or null when the exam has no due date. - `examCategory` (enum(Homework | Classwork | Quiz | Assignment | Mock | Midterm | Final | Past Papers) | null, optional) — Optional exam category, or null. - `examTypeId` (string | null, optional) — Optional existing exam-type identifier, or null. **Request body example** ```json { "examName": "Midterm Exam", "maxGrade": 100, "examPaperPdfUrl": "https://files.example.com/midterm-questions.pdf", "markSchemePdfUrl": "https://files.example.com/midterm-scheme.pdf", "gradingMode": "standard", "isGradesPublished": false } ``` **Example request** ```bash curl -X POST "https://thelearningmate.com/api/public/v1/courses/c_9f8a7b6c5d4e/exams" \ -H "Authorization: Bearer pk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"examName":"Midterm Exam","maxGrade":100,"examPaperPdfUrl":"https://files.example.com/midterm-questions.pdf","markSchemePdfUrl":"https://files.example.com/midterm-scheme.pdf","gradingMode":"standard","isGradesPublished":false}' ``` **Response data fields** - `examId` (string, required) - `status` (enum(processing), required) - `pollUrl` (string, required) **Example response (202)** ```json { "success": true, "data": { "examId": "e_1a2b3c4d5e6f", "status": "processing", "pollUrl": "/api/public/v1/exams/e_1a2b3c4d5e6f" } } ``` **Additional errors** - `400 INVALID_EXAM_TYPE` — The supplied exam type does not exist. - `503 FINALIZATION_DISPATCH_FAILED` — The background finalization workflow could not be started. The request is safe to retry. --- ## POST /api/public/v1/courses/{courseId}/students — Create & enroll students Creates and enrolls up to 100 students in a course in one request. Students that already exist are re-used and simply enrolled. Requires ownership of the course. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 10 requests / minute (per API key) **Notes** - Each student must include a `name` (min 2 chars) plus at least one of `email` or `mobileNumber`. - A maximum of 100 students may be created per request. **Path parameters** - `courseId` (string, required) **Request body fields** - `students` (array, required) — Students to create or enroll in the course. - `students[].name` (string, required) — Student name within the course. - `students[].email` (string, optional) — Student email; either email or mobileNumber is required. - `students[].mobileNumber` (string, optional) — Student mobile number; either mobileNumber or email is required. - `students[].parentMobileNumber` (string, optional) — Optional parent mobile number. - `students[].class` (string, optional) — Optional class or section. - `students[].gender` (enum(male | female) | string, optional) — Optional student gender. - `students[].grade_level` (string, optional) — Optional grade level. - `students[].image` (string, optional) — Optional profile-image URL. **Request body example** ```json { "students": [ { "name": "Jane Doe", "email": "jane.doe@example.com" }, { "name": "John Roe", "mobileNumber": "01000000000" } ] } ``` **Example request** ```bash curl -X POST "https://thelearningmate.com/api/public/v1/courses/c_9f8a7b6c5d4e/students" \ -H "Authorization: Bearer pk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"students":[{"name":"Jane Doe","email":"jane.doe@example.com"},{"name":"John Roe","mobileNumber":"01000000000"}]}' ``` **Response data fields** - `newStudentsCount` (integer, required) - `existingStudentsCount` (integer, required) - `totalEnrolled` (integer, required) - `insertedStudents` (array, required) - `insertedStudents[].id` (string, required) - `insertedStudents[].name` (string | null, required) - `existingStudentsInCourse` (array, required) - `existingStudentsInCourse[].id` (string, required) - `existingStudentsInCourse[].name` (string | null, required) - `existingStudentsInCourse[].email` (string | null, required) - `existingStudentsInCourse[].mobileNumber` (string | null, required) **Example response (201)** ```json { "success": true, "data": { "newStudentsCount": 1, "existingStudentsCount": 1, "totalEnrolled": 2, "insertedStudents": [ { "id": "s_aaa111bbb222", "name": "Jane Doe" } ], "existingStudentsInCourse": [ { "id": "s_ccc333ddd444", "name": "John Roe", "email": null, "mobileNumber": "01000000000" } ] } } ``` **Additional errors** - `400 DUPLICATE_STUDENTS` — The request contains duplicate email addresses or mobile numbers. - `400 STUDENT_CREATION_FAILED` — The students could not be created or enrolled. --- ## PATCH /api/public/v1/courses/{courseId}/students/{studentId} — Update a student Partially updates an enrolled student. Identity fields apply globally, while name, class, grade level, and parent mobile number apply only within this course. An optional password updates the linked student login or creates and links one when needed. Requires ownership of the course and an active enrollment. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 30 requests / minute (per API key) **Notes** - Provide at least one field. - Use `null` to clear nullable fields. - Passwords must contain at least 8 characters and are write-only. **Path parameters** - `courseId` (string, required) - `studentId` (string, required) **Request body fields** - `name` (string, optional) — Course-scoped student display name. - `email` (string | null, optional) — Global student email; use null to clear it. - `mobileNumber` (string | null, optional) — Global student mobile number; use null to clear it. - `parentMobileNumber` (string | null, optional) — Course-scoped parent mobile number; use null to clear it. - `class` (string | null, optional) — Course-scoped class or section; use null to clear it. - `gender` (enum(male | female) | null, optional) — Global student gender; use null to clear it. - `grade_level` (string | null, optional) — Course-scoped grade level; use null to clear it. - `image` (string | null, optional) — Global student profile-image URL; use null to clear it. - `password` (string, optional) — Write-only login password. Updates the linked login or creates one. **Request body example** ```json { "name": "Jane Smith", "class": "4B", "password": "new-password-123" } ``` **Example request** ```bash curl -X PATCH "https://thelearningmate.com/api/public/v1/courses/c_9f8a7b6c5d4e/students/s_aaa111bbb222" \ -H "Authorization: Bearer pk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"name":"Jane Smith","class":"4B","password":"new-password-123"}' ``` **Response data fields** - `studentId` (string, required) - `courseId` (string, required) - `name` (string, required) - `email` (string | null, required) - `mobileNumber` (string | null, required) - `parentMobileNumber` (string | null, required) - `class` (string | null, required) - `gender` (enum(male | female) | null, required) - `grade_level` (string | null, required) - `image` (string | null, required) - `passwordUpdated` (boolean, required) **Example response (200)** ```json { "success": true, "data": { "studentId": "s_aaa111bbb222", "courseId": "c_9f8a7b6c5d4e", "name": "Jane Smith", "email": "jane.doe@example.com", "mobileNumber": null, "parentMobileNumber": null, "class": "4B", "gender": "female", "grade_level": "10", "image": null, "passwordUpdated": true } } ``` **Additional errors** - `409 DUPLICATE_EMAIL` — The email is already assigned to another student. - `409 DUPLICATE_MOBILE_NUMBER` — The mobile number is already assigned to another student. - `409 STUDENT_LOGIN_CONTACT_REQUIRED` — A password was supplied, but the student has no login contact. - `409 STUDENT_LOGIN_CONFLICT` — The student contact or linked login conflicts with another account. --- ## DELETE /api/public/v1/courses/{courseId}/students/{studentId} — Remove a student from a course Removes the student enrollment and that student’s exam data for this course. The global student record, login, and enrollments in other courses are retained. Requires ownership of the course and an active enrollment. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 30 requests / minute (per API key) **Path parameters** - `courseId` (string, required) - `studentId` (string, required) **Example request** ```bash curl -X DELETE "https://thelearningmate.com/api/public/v1/courses/c_9f8a7b6c5d4e/students/s_aaa111bbb222" \ -H "Authorization: Bearer pk_your_api_key_here" ``` **Response data fields** - `deletedStudentId` (string, required) - `deletedStudentName` (string, required) **Example response (200)** ```json { "success": true, "data": { "deletedStudentId": "s_aaa111bbb222", "deletedStudentName": "Jane Smith" } } ``` --- ## GET /api/public/v1/exams/{examId} — Get an exam Returns the status and metadata of a single exam. This is also the poll endpoint for exam creation: `status` is `processing` while the exam is being prepared, `ready` once it is available, or `failed` if it could not be prepared. Requires ownership of the exam. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 120 requests / minute (per API key) **Path parameters** - `examId` (string, required) **Example request** ```bash curl -X GET "https://thelearningmate.com/api/public/v1/exams/e_1a2b3c4d5e6f" \ -H "Authorization: Bearer pk_your_api_key_here" ``` **Response data fields** - `examId` (string, required) - `courseId` (string, required) - `name` (string, required) - `maxGrade` (number, required) - `status` (enum(processing | ready | failed), required) - `isCreating` (boolean, required) - `isGradesPublished` (boolean, required) - `isStudentUploadEnabled` (boolean, required) - `isMarkSchemeVisible` (boolean, required) - `isVisibleToNewStudents` (boolean, required) - `gradingMode` (enum(instant | standard | premium), required) - `examCategory` (enum(Homework | Classwork | Quiz | Assignment | Mock | Midterm | Final | Past Papers) | null, required) - `examTypeId` (string | null, required) - `dueDateTime` (string | null, required) - `createdAt` (string | null, required) - `updatedAt` (string, required) **Example response (200)** ```json { "success": true, "data": { "examId": "e_1a2b3c4d5e6f", "courseId": "c_9f8a7b6c5d4e", "name": "Midterm Exam", "maxGrade": 100, "status": "ready", "isCreating": false, "isGradesPublished": true, "isStudentUploadEnabled": false, "isMarkSchemeVisible": false, "isVisibleToNewStudents": false, "gradingMode": "standard", "examCategory": "Midterm", "examTypeId": null, "dueDateTime": null, "createdAt": "2026-05-14T09:30:00.000Z", "updatedAt": "2026-05-14T09:30:00.000Z" } } ``` --- ## PATCH /api/public/v1/exams/{examId} — Update an exam Partially updates non-artifact exam metadata and controls. For exams published to Google Classroom, changes to name, maximum grade, or due date are synchronized to Classroom before the local update is committed. Requires ownership of the exam. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 30 requests / minute (per API key) **Notes** - Provide at least one field. - Use `null` to clear `examCategory`, `examTypeId`, or `dueDateTime`. - Question-paper and mark-scheme artifacts cannot be changed through this endpoint. - Per-student visibility remains on the dedicated student visibility endpoint. **Path parameters** - `examId` (string, required) **Request body fields** - `name` (string, optional) - `maxGrade` (integer, optional) - `examCategory` (enum(Homework | Classwork | Quiz | Assignment | Mock | Midterm | Final | Past Papers) | null, optional) - `examTypeId` (string | null, optional) - `dueDateTime` (string | null, optional) - `gradingMode` (enum(instant | standard | premium), optional) - `isGradesPublished` (boolean, optional) - `isStudentUploadEnabled` (boolean, optional) - `isMarkSchemeVisible` (boolean, optional) - `isVisibleToNewStudents` (boolean, optional) **Request body example** ```json { "name": "Updated Midterm", "maxGrade": 120, "examCategory": "Midterm", "isGradesPublished": true } ``` **Example request** ```bash curl -X PATCH "https://thelearningmate.com/api/public/v1/exams/e_1a2b3c4d5e6f" \ -H "Authorization: Bearer pk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"name":"Updated Midterm","maxGrade":120,"examCategory":"Midterm","isGradesPublished":true}' ``` **Response data fields** - `examId` (string, required) - `courseId` (string, required) - `name` (string, required) - `maxGrade` (number, required) - `isCreating` (boolean, required) - `isGradesPublished` (boolean, required) - `isStudentUploadEnabled` (boolean, required) - `isMarkSchemeVisible` (boolean, required) - `isVisibleToNewStudents` (boolean, required) - `gradingMode` (enum(instant | standard | premium), required) - `examCategory` (enum(Homework | Classwork | Quiz | Assignment | Mock | Midterm | Final | Past Papers) | null, required) - `examTypeId` (string | null, required) - `dueDateTime` (string | null, required) - `createdAt` (string | null, required) - `updatedAt` (string, required) **Example response (200)** ```json { "success": true, "data": { "examId": "e_1a2b3c4d5e6f", "courseId": "c_9f8a7b6c5d4e", "name": "Updated Midterm", "maxGrade": 120, "isCreating": false, "isGradesPublished": true, "isStudentUploadEnabled": false, "isMarkSchemeVisible": false, "isVisibleToNewStudents": false, "gradingMode": "standard", "examCategory": "Midterm", "examTypeId": null, "dueDateTime": null, "createdAt": "2026-05-14T09:30:00.000Z", "updatedAt": "2026-05-15T10:00:00.000Z" } } ``` **Additional errors** - `400 INVALID_EXAM_TYPE` — The supplied exam type does not exist. - `400 INVALID_DUE_DATE` — A linked Google Classroom assignment rejected the due date. - `502 GOOGLE_CLASSROOM_SYNC_FAILED` — The Classroom update failed, so the local exam was not changed. --- ## DELETE /api/public/v1/exams/{examId} — Delete an exam Deletes an exam and its local mappings. Cleanup of linked Google Classroom coursework, its uploaded Drive file, and exam-owned S3 artifacts is best-effort and reported in the successful response. Requires ownership of the exam. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 5 requests / hour (per API key) **Path parameters** - `examId` (string, required) **Example request** ```bash curl -X DELETE "https://thelearningmate.com/api/public/v1/exams/e_1a2b3c4d5e6f" \ -H "Authorization: Bearer pk_your_api_key_here" ``` **Response data fields** - `deletedExamId` (string, required) - `deletedExamName` (string, required) - `cleanup` (object, required) **Example response (200)** ```json { "success": true, "data": { "deletedExamId": "e_1a2b3c4d5e6f", "deletedExamName": "Updated Midterm", "cleanup": { "googleClassroomCoursework": "deleted", "googleDriveFile": "deleted", "s3": { "deletedCount": 6, "failedCount": 0, "preservedCount": 0 } } } } ``` --- ## PATCH /api/public/v1/exams/{examId}/students/visibility — Update exam visibility per student Batch-updates whether an exam is visible to specific students, matched by email. Emails with no matching student or enrollment are reported in `notFound`; per-row failures are reported in `errors` without aborting the batch. Requires ownership of the exam. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 30 requests / minute (per API key) **Notes** - Up to 100 updates may be submitted per request. **Path parameters** - `examId` (string, required) **Request body fields** - `studentUpdates` (array, required) — Per-student visibility updates matched by email. - `studentUpdates[].email` (string, required) - `studentUpdates[].isVisible` (boolean, required) **Request body example** ```json { "studentUpdates": [ { "email": "jane.doe@example.com", "isVisible": true }, { "email": "john.roe@example.com", "isVisible": false } ] } ``` **Example request** ```bash curl -X PATCH "https://thelearningmate.com/api/public/v1/exams/e_1a2b3c4d5e6f/students/visibility" \ -H "Authorization: Bearer pk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"studentUpdates":[{"email":"jane.doe@example.com","isVisible":true},{"email":"john.roe@example.com","isVisible":false}]}' ``` **Response data fields** - `examId` (string, required) - `updated` (integer, required) - `notFound` (array, required) - `errors` (array, required) **Example response (200)** ```json { "success": true, "data": { "examId": "e_1a2b3c4d5e6f", "updated": 1, "notFound": [ "john.roe@example.com" ], "errors": [] } } ``` **Additional errors** - `400 VISIBILITY_UPDATE_FAILED` — The visibility batch could not be applied. --- ## POST /api/public/v1/exams/{examId}/grades/query — Query grades by email Looks up grades for a set of students by email address for a single exam. Unknown emails are returned with `grade: null` rather than failing the request. Requires ownership of the exam. - Authentication: `Authorization: Bearer pk_your_api_key_here` - Rate limit: 60 requests / minute (per API key) **Notes** - Up to 100 emails may be queried per request. - A grade is withheld (`grade: null`) until that student’s expected grading-completion date has passed; the pending date is then returned in `expectedGradingCompletionDate`. **Path parameters** - `examId` (string, required) **Request body fields** - `studentEmails` (array, required) — Student emails to query, returned in the same order. **Request body example** ```json { "studentEmails": [ "jane.doe@example.com", "john.roe@example.com" ] } ``` **Example request** ```bash curl -X POST "https://thelearningmate.com/api/public/v1/exams/e_1a2b3c4d5e6f/grades/query" \ -H "Authorization: Bearer pk_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"studentEmails":["jane.doe@example.com","john.roe@example.com"]}' ``` **Response data fields** - `examId` (string, required) - `totalStudentsRequested` (integer, required) - `results` (array, required) - `results[].studentEmail` (string, required) - `results[].studentName` (string, required) - `results[].grade` (number | null, required) - `results[].maxGrade` (number, required) - `results[].answerPaperPdfUrl` (string | null, required) - `results[].isGradePublished` (boolean, required) - `results[].expectedGradingCompletionDate` (string | null, required) **Example response (200)** ```json { "success": true, "data": { "examId": "e_1a2b3c4d5e6f", "totalStudentsRequested": 2, "results": [ { "studentEmail": "jane.doe@example.com", "studentName": "Jane Doe", "grade": 87.5, "maxGrade": 100, "answerPaperPdfUrl": "https://files.example.com/jane-answers.pdf", "isGradePublished": true, "expectedGradingCompletionDate": null }, { "studentEmail": "john.roe@example.com", "studentName": "Unknown", "grade": null, "maxGrade": 100, "answerPaperPdfUrl": null, "isGradePublished": true, "expectedGradingCompletionDate": null } ] } } ``` **Additional errors** - `400 GRADE_QUERY_FAILED` — The grade lookup could not be completed.