Developer docs
Feedback Flows API
Mark work, check integrity, sync a roster, and pull results back into your LMS or MIS. Every call uses the same key; sandbox keys return mocks and never charge credits.
Base URL https://feedbackflows.org/api/v1 · Header X-API-Key
Getting started
- Get a key (see below). Store it as a secret; it is shown once.
- Send it on every request:
X-API-Key: YOUR_KEY. - Call
GET /account/statusto confirm the key and remaining credits. - Call a cheap analysis endpoint, or list students, then move on to jobs.
How to get a key
- Site admin still issues keys at
/admin/api-keys(the existing flow). Use this for non-enterprise integrators and onboarding sandbox keys. - Enterprise Owners and Managers with the Integrations feature enabled can issue, scope, rotate and revoke keys for their own organisation on the console Integrations tab. Site admin still sees every key.
- Keys are hashed at rest. Copy the secret immediately; we cannot show it again.
- Sandbox keys skip billing and return canned responses for AI and job endpoints, so you can wire an integration without spending credits.
Scopes on a key: marking, integrity, analytics, media, admin. The admin scope implies the others. A missing scope returns 403 with SCOPE_FORBIDDEN.
First call
cURL
curl -X GET 'https://feedbackflows.org/api/v1/account/status' \ -H 'X-API-Key: YOUR_API_KEY'
Python
import requests
r = requests.get(
"https://feedbackflows.org/api/v1/account/status",
headers={"X-API-Key": "YOUR_API_KEY"},
)
print(r.status_code, r.json())
Credits, limits, and idempotency
- Usage is billed in credits against the account that owns the key.
credits_remainingof-1means unlimited. - Synchronous routes: 60 requests per minute per key. Job submissions: 10 per hour. A
429includesRetry-After. Rejected calls are not counted. - Async job POSTs accept an
Idempotency-Keyheader. Retry the same key for 24 hours and you get the original202instead of a second charge. - JSON responses include a
request_idyou can quote to support.
Try it
Paste a key and send a real request from this page. Live keys spend credits; use a sandbox key while you are wiring things up. The key is kept in sessionStorage in this browser only.
/api/v1. No proxy.Response
{ }
cURL for this request
Dedicated testers
Standalone pages with forms for ZIP uploads and longer workflows. Each page can be downloaded and run against production or a local server.
API v2 testers: style comparison · cost estimate · exports · AI tutor jobs · worksheet jobs · reference-check jobs · interactive-feedback jobs
Marking
Integrity
Jobs
LMS / data
Endpoint reference
Paths below are relative to https://feedbackflows.org/api/v1. Field names match the live handlers.
Account
GET /test
Confirms the key is valid. No scope required.
GET /account/status
Returns business_name, api_key_status, and credits_remaining.
{
"business_name": "Northbridge College",
"api_key_status": "active",
"credits_remaining": 985
}
POST /jobs/estimate-cost
Body: job_type (batch_style, rubric_marking, automarking, media, exam, learner_pack, smart_essay, ai_detector, worksheet, or reference_check), optional file_count / student_count.
Marking
POST /interactive-feedback · scope marking
{
"student_text": "The industrial revolution was a turning point...",
"marking_criteria": ["Clarity of argument", "Use of evidence"]
}
POST /smart-essay-marker · scope marking
Synchronous. Prefer POST /smart-essay/jobs for long scripts.
{
"essay_text": "The full essay...",
"marking_criteria": ["Clarity of argument", "Use of evidence"]
}
POST /ai-tutor · scope marking
Body: student_draft, optional mode (general or rubric), marking_rubric when mode is rubric, persona (encouraging or direct).
POST /students/{student_id}/worksheet · scope marking
Body: optional weaknesses_list, format (short_answer, …), level, num_questions. If weaknesses are omitted, history is used.
Integrity
POST /ai-detector · scope integrity
Body: document_text, optional academic_mode. Prefer POST /ai-detector/jobs for long documents.
POST /reference-check (legacy) · scope integrity
Simplified detector. New integrations should use /ai-detector or the job.
POST /full-reference-check · scope integrity
Body: document_text, style (APA, Harvard, MLA, Chicago), optional enable_semantic_check.
POST /style-comparison · scope integrity
Body: student_id, submission_text. The student must already have writing samples on file.
Jobs
All create endpoints return 202 with job_id and a status path. Optional callback_url (HTTPS) and Idempotency-Key header apply unless noted. ZIP bodies use zip_base64.
POST /batch-style/jobs · GET /batch-style/jobs/{job_id}
POST /rubric-marking/jobs · GET /rubric-marking/jobs/{job_id} · POST /rubric-marking/jobs/{job_id}/cancel
Create body includes rubric_id, zip_base64, optional filename, marking_style, grading_system.
POST /automarking/jobs · GET /automarking/jobs/{job_id}
Body: questions (each with question_text, available_marks, mark_scheme) or a saved template, plus zip_base64.
POST /exam-marker/jobs
Body: student_id, zip_base64. Status is polled on GET /automarking/jobs/{job_id}.
POST /media-marker/jobs · GET /media-marker/jobs/{job_id} · scope media
Body: criteria, media_base64, optional filename, education_level, persona, presentation_type.
POST /smart-essay/jobs · GET /smart-essay/jobs/{job_id}
Body: essay_text, marking_criteria, optional education_level, persona.
POST /ai-detector/jobs · GET /ai-detector/jobs/{job_id} · scope integrity or marking
Body: document_text, optional academic_mode.
POST /learner-packs/jobs · GET /learner-packs/jobs/{job_id}
{
"student_id": 41,
"template_id": 3,
"selected_result_refs": [
{"kind": "auto", "id": 120},
{"kind": "rubric", "id": 88}
],
"callback_url": "https://example.edu/hooks/feedback-flows"
}
POST /ai-tutor/jobs · GET /ai-tutor/jobs/{job_id}
POST /worksheets/jobs · GET /worksheets/jobs/{job_id}
POST /reference-check/jobs · GET /reference-check/jobs/{job_id}
POST /interactive-feedback/jobs · GET /interactive-feedback/jobs/{job_id}
Exports
/rubric-marking/jobs/{id}/export.csv · /rubric-marking/results/{id}/export.pdf
/automarking/jobs/{id}/export.csv · /automarking/results/{id}/export.pdf
/smart-essay/jobs/{id}/export.pdf · /learner-packs/jobs/{id}/export.pdf
/media-marker/jobs/{id}/export.pdf · /worksheets/jobs/{id}/export.pdf
/reference-check/jobs/{id}/export.pdf · /classes/{id}/export.csv
Enterprise results awaiting required human review return 403 REVIEW_REQUIRED.
Illustrative credit costs
Defaults: style comparison 1/file; rubric and automarking 10/student; media 20; exam 5/student; learner pack 3; smart essay 10; AI detector 1; worksheet 3; reference check 5. Server configuration may override these values.
Roster and results
GET / POST /students · PATCH /students/{student_id}
Create: name, optional uln, class_ids. Duplicate names return 409. Org seat limits return 403.
GET / POST /classes · PATCH /classes/{class_id}
Create: name, optional subject. Patch may set concluded.
PATCH /classes/{class_id}/students
{
"add_student_ids": [41, 42],
"remove_student_ids": [17]
}
GET /classes/{class_id}/results · scope analytics
Paginated rubric and automarking outcomes for every student in the class.
GET / POST /rubrics · GET /rubrics/{rubric_id}
Create: name, criteria (string or list of strings), optional marking_guidance, pass_rate.
Also: GET /classes/{class_id}/assignments, GET /question-templates, GET /students/{id}/ai-history, POST /students/{id}/progress-report, POST /classes/{id}/assignments/{id}/analyze (analytics scope).
List endpoints accept ?page=, ?per_page=, and ?search=.
Webhooks
Pass callback_url when you create a job. When the job finishes we POST JSON to that URL.
- Header
X-FeedbackFlows-Signaturein the formt=<unix>,v1=<hex>. - HMAC-SHA256 over
<timestamp>.<raw-body>using the key’s webhook secret. - Reject deliveries whose timestamp is more than five minutes old. That is what stops a captured request being replayed.
- Header
X-FeedbackFlows-Deliveryis a UUID; use it to ignore retries. - Callback URLs must be public HTTPS. We re-resolve DNS at delivery time and refuse private or loopback addresses.
import hashlib, hmac, time
def verify(header, raw_body, secret, tolerance=300):
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(parts["v1"], expected)
Errors
| Code | Meaning |
|---|---|
200 | Success. |
201 | Resource created (student, class, rubric). |
202 | Job queued. |
400 | Missing or invalid fields. |
401 | Missing, invalid, or expired key. |
402 | Not enough credits. |
403 | Scope missing, demo lock, or student limit. |
404 | Resource not found. |
409 | Conflict (duplicate student, concluded class). |
413 | Payload too large. |
429 | Rate limited; see Retry-After. |
500 | Unexpected server error. Quote request_id. |
Error bodies are JSON: error, code, status, request_id. They are never HTML.