Quanta logoQuanta API

Last updated: August 28, 2026

Introduction

At the core of our platform is the Quanta API, an algorithm that does LLM-powered reviews of input solutions. It handles inputs in various formats, including text, images, PDFs, or a mix of all three, and its focus is on evaluating reasoning, not just final answers. In fact, a correct answer with no explanation will receive the lowest possible grade.

Two-letter grading system

Each submission receives a two-letter grade (e.g. "AB") on a scale from A (Perfect Enough) to F (No meaningful progress). The letters used are A, B, C, E and F — there is deliberately no D, so both characters of a grade always come from those five:

First letter

Focuses on technical correctness, even if the solution is short or has minor gaps.

Second letter

Focuses on the clarity and consistency of the logical flow.

It is up to you how you want to map this overall grade to a number (i.e. points), here are the two most common options we use internally:

0–7 Scale

GradePoints
AA7
AB6
BA5
AC, BB4
BC, CA, CB3
AE, CC2
AF, BE, CE, EA, EB, EC1
BF, CF, EE, EF, FA, FB, FC, FE, FF0

0–1 Scale

GradePoints
AA, AB, BA1
Everything else0

After lots of tests and adjustments, we're ready to open this API to our beta partners. This documentation explains how to use it.

Setting Expectations

The API is not perfect, but has proven highly useful — particularly for easier tasks with lenient mode enabled, which covers the majority of most teachers' time spent on reviewing students' work.

1.Feedback quality: On average, ~80% of feedback is fully acceptable and ~5% is not great. The remaining ~15% falls somewhere in between and varies by subdomain and input type, e.g. geometry and/or handwriting tend to score lower.
2.Response time: No images: ~20-40 seconds. With images: ~30-70 seconds (each image adds processing time).
3.Price: ~$0.20 per text-only submission and ~$0.30 with an image. This is the billed amount and varies with the length of the problem and the solution. Submissions rejected at the sanity check cost ~$0.07.

Note: this is the worst this model will ever be. It will only get cheaper, faster, and more accurate over time — though that takes feedback and iteration.

API Overview

Base URL

https://api.quanta.world

Requirements

FeatureDescription
ProtocolHTTPS (REST)
Request formatJSON or multipart/form-data (CSV upload for bulk review)
Response formatJSON
AuthenticationBearer token (API key)

Available Endpoints

MethodEndpointAuth RequiredDescription
GET/api/healthNoService health check
POST/api/reviewYesSingle solution AI review
POST/api/bulk-reviewYesSubmit multiple solutions for async review
GET/api/jobs/:jobIdYesCheck bulk job status and results
GET/api/jobs/:jobId/exportYesDownload bulk job results as CSV
GET/api/filesYesRead an image or file referenced by your own reviews

GET /api/health

No authentication required. Use this to verify the API is reachable and all dependencies are operational before making substantive calls.

Response Fields

FieldTypeDescription
status"ok" | "degraded"Overall service health
uptime_msnumberMilliseconds since the server process started
timestampstringISO 8601 timestamp of the response
checks.databaseobjectSupabase connectivity status
checks.environmentobjectRequired environment variable status
{
  "status": "ok",
  "uptime_ms": 3567429,
  "timestamp": "2026-03-12T20:35:25.711Z",
  "checks": {
    "database": {
      "status": "ok"
    },
    "environment": {
      "status": "ok"
    }
  }
}
Example Request
curl https://api.quanta.world/api/health

Authentication

All protected endpoints require an API key passed as a Bearer token in the Authorization header.

Authorization: Bearer <your-api-key>

Getting an API Key

  1. Log in at api.quanta.world
  2. Navigate to API Keys
  3. Click Add Key — optionally set a nickname and expiration date
  4. Copy your key immediately — it is only shown in full once

You can create multiple API keys, each with its own nickname and optional expiration date. Keys can be deactivated or deleted at any time from the API Keys page. Deactivated or expired keys will return a 401 Unauthorized error.

Permissions

Each API key carries a permissions object:

json
{
  "single_review": true,
  "bulk_review": true
}
PermissionDescription
single_reviewSubmit one solution at a time for review
bulk_reviewSubmit multiple solutions in a single request

Usage Limits

Two limits apply to every API key:

LimitValueDescription
Bulk upload size100 rows · 10 MBPer /api/bulk-review request. A larger CSV is rejected with 400.
Daily reviews500 per dayReviewed input solutions per key per UTC day, counting both single and bulk. Exceeding it returns 429 and the count resets at midnight UTC.

The daily limit can be raised per key — contact us if 500 is not enough. A bulk upload is checked against the limit as a whole, so a 60-row CSV is refused outright if only 50 reviews remain for the day rather than being partially processed.

POST /api/review

Submit a single student solution for AI review. Requires the single_review permission.

Request Body

The /api/review endpoint accepts three primary components:task (the underlying problem), input_solution (the student's work), and reviewing_parameters (to calibrate strictness and style).

Top-level Fields

FieldTypeRequiredDescription
taskobjectYesThe problem definition (see below)
input_solutionPromptBlock[]YesThe student's solution — text, images, or files
reviewing_parametersobjectNoOptional grading parameters (see below)

task Object

FieldTypeRequiredDescription
contentPromptBlock[]YesThe problem statement shown to the student
correct_solutionsarrayYesReference solutions to help the AI grade (see below)
is_context_requiredbooleanNoWhether extra context is needed. Default: false
context_detailsPromptBlock[]NoExtra context blocks. Use [] if not needed

correct_solutions[] Items

FieldTypeRequiredDescription
sloganstringYesShort label for this approach, e.g. "Direct proof"
contentPromptBlock[]YesThe full reference solution content

PromptBlock Object

Used by task.content, input_solution, correct_solutions[].content, and context_details.

FieldTypeRequiredDescription
typestringYesOne of "text", "image_url", "image", "file"
textstringIf type is "text"The text content (supports LaTeX)
image_url.urlstring (URL)If type is "image_url"Publicly accessible image URL
file.file_idstringIf type is "file"File ID for an uploaded file
file_urlstring (URL)If type is "file"Publicly accessible file URL

reviewing_parameters Object

FieldTypeRequiredDefaultDescription
strictnessstringNo"auto"Grading strictness — see values below
cost_efficiency_modestringNo"standard"Model quality/cost tier — see values below
aux_rubricobjectNo{"validity": "-", "quality": "-"}Custom grading rubric
aux_rubric.validitystringNo"-"What makes an answer correct
aux_rubric.qualitystringNo"-"What makes a presentation good
num_sanity_rerunsinteger (1–10)No3How many sanity check reruns for confidence

strictness Values

ValueDescription
"auto"The model decides based on the problem. Balanced — not too strict, not too kind. Best for most use-cases.
"lenient"Forgives minor mistakes. Thinks from the standpoint of 'What could the student have meant?'. Avoids downgrading unless there are major gaps.
"strict"Even minor errors are flagged and likely lead to a downgrade. Not commonly needed since "auto" is already relatively strict.

cost_efficiency_mode Values

ValueDescription
"standard"The default tier. Balanced quality and cost. Best for most use-cases.
"economy"Cheaper and faster. Suited to simpler content where deep review adds little.
"thorough"The most capable review. Suited to harder content (olympiad-level, long proofs).

Response

A successful review returns the overall grade, detailed feedback per dimension, and cost/timing metadata.

json
{
  "success": true,
  "feedback_type": "AI-generated",
  "overall_grade": "BA",
  "model_output": {
    "status": "Sanity Checks Passed, Full Review Completed",
    "sanity": {
      "sanity_status": "Pass",
      "sanity_status_justification": "...",
      "sanity_confidence_level": "98%",
      "sanity_statuses_reruns": "Pass, Pass, Pass"
    },
    "validity": {
      "validity_grade": "B",
      "nontrivial_mistakes_or_unjustified_claims": "...",
      "explanation_good_aspects_summary": "..."
    },
    "quality": {
      "quality_grade": "A",
      "major_clarity_issues": "...",
      "presentation_and_formatting": "...",
      "quality_good_aspects": "..."
    },
    "feedback_summary": "Your proof correctly uses the definition of even numbers..."
  },
  "metadata": {
    "model_name": "quanta-senji-30-09-2025",
    "bills": {
      "total_bill_usd": 0.1465,
      "sanity_total_bill_usd": 0.0316,
      "validity_total_bill_usd": 0.0704,
      "quality_total_bill_usd": 0.0358,
      "attachment_processing_bill_usd": 0
    },
    "times": {
      "time_taken_to_review_s": "23.03",
      "time_spent_on_sanity_check_s": "5.22",
      "time_spent_on_solution_refinement_s": "4.35",
      "time_spent_on_validity_review_s": "10.24",
      "time_spent_on_quality_review_s": "3.90",
      "time_spent_on_feedback_summary_s": "3.22"
    },
    "timestamp": "2026-03-12T20:42:51.572Z"
  }
}
FieldTypeDescription
successbooleanWhether the request was processed successfully
overall_gradestringTwo-character grade: first character is validity (AF), second is quality (AF). Example: BA = validity B, quality A
model_output.statusstringPipeline status — indicates whether sanity check passed and full review was completed
model_output.sanityobjectSanity check result: sanity_status is "Pass" or "Fail". If failed, the review stops early with grade FF
model_output.validityobjectCorrectness assessment: includes validity_grade (AF), mistakes found, and good aspects
model_output.qualityobjectPresentation assessment: includes quality_grade (AF), clarity issues, and formatting notes
model_output.feedback_summarystringHuman-readable feedback summary suitable for showing to the student
metadata.billsobjectBilled amount in USD for each pipeline stage. total_bill_usd is the amount charged. Note that attachment_processing_bill_usd covers only the call that reads an image or PDF into text; the attachment is also included in the review stages, so it costs more than that one line suggests.
metadata.timesobjectTiming breakdown in seconds for each pipeline stage
prerun_checks_logsobjectTop-level diagnostic trace of the checks run before grading. Present on success and on some errors. Useful when reporting a problem to us; safe to ignore otherwise, and its shape is not part of the stable contract.

The sub-fields inside validity and quality vary by review — for example major_conceptual_errors and the *_grades_reruns and *_confidence_level fields appear only when the corresponding stage produced them. Read them defensively rather than assuming a fixed set. When the sanity check fails, validity and quality are the string "N/A" rather than objects.

Example 1: Text-based Solution Review

Combinatorics problem with a text-based student solution.

Text-based Review
curl -X POST https://api.quanta.world/api/review \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "task": {
      "content": [
        {
          "type": "text",
          "text": "How many six-digit numbers contain exactly three zeros in their decimal representation, with the additional constraint that no two zeros are adjacent? Note: numbers do not begin with a zero."
        }
      ],
      "is_context_required": false,
      "context_details": [],
      "correct_solutions": [
        {
          "slogan": "Direct Computation",
          "content": [
            {
              "type": "text",
              "text": "**Answer:** $9^3 = 729$\n\n**Explanation:**\n1. Note that since there are 3 non-adjacent zeros, they have to be in places 2, 4, 6.\n2. The remaining 3 digits can each be 1–9, giving $9 \\cdot 9 \\cdot 9 = 729$."
            }
          ]
        }
      ]
    },
    "input_solution": [
      {
        "type": "text",
        "text": "Firstly, there are 9-1=8 possibilities for the first digit since it cannot be zero. Now, the zeros can'\''t be adjacent so they must be in positions 2, 4 and 6, and for the remaining 2 positions there are 10*10 options. However, we counted some options twice, specifically where there are non-zeros in positions 3 and 5. Thus N = 800 - 9x9 = 729."
      }
    ],
    "reviewing_parameters": {
      "aux_rubric": {
        "validity": "No points should be deducted for the lack of explanation of why zeros must be in positions 2, 4 and 6, this is counted as trivial.",
        "quality": "This is an easy and short problem, so there is NO need to be too judgemental about the presentation!"
      },
      "strictness": "auto",
      "cost_efficiency_mode": "standard"
    }
  }'

Example 2: Unrelated Image Solution (Sanity Fail)

When the student submits an image that is completely unrelated to the problem, the sanity check fails early and the review returns with an FF grade. This example also shows is_context_required and context_details usage.

Unrelated Image — Sanity Fail
curl -X POST https://api.quanta.world/api/review \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "task": {
      "content": [
        {
          "type": "text",
          "text": "Following that other problem, what is the answer if we need 2 or 5 instead of 1 or 4?"
        }
      ],
      "is_context_required": true,
      "context_details": [
        {
          "type": "text",
          "text": "That other problem refers to: There is a six-sided die with numbers 1, 2, ..., 6 on its faces. Find the probability of rolling 1 or 4. So this problem is the same, just 2 and 5 instead of 1 or 4."
        }
      ],
      "correct_solutions": [
        {
          "slogan": "Direct computation, nothing fancy",
          "content": [
            {
              "type": "text",
              "text": "**Answer:** 1/3\n\n**Explanation:** There are six possibilities. Precisely two are of interest. Hence the answer is 2/6 = 1/3."
            }
          ]
        }
      ]
    },
    "input_solution": [
      {
        "type": "image_url",
        "image_url": {
          "url": "https://ujngnciigcpczmsloboz.supabase.co/storage/v1/object/public/quanta-public/ai-model-test-images/image-example.png"
        }
      }
    ],
    "reviewing_parameters": {
      "aux_rubric": {
        "validity": "-",
        "quality": "-"
      },
      "strictness": "auto",
      "cost_efficiency_mode": "standard"
    }
  }'

Example 3: Images in Task and Solution

Both the problem statement and the student's solution can contain images. Use image_url blocks in task.content, correct_solutions[].content, and input_solution.

Images in Task & Solution
curl -X POST https://api.quanta.world/api/review \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "task": {
      "content": [
        {
          "type": "text",
          "text": "Is it possible to form a square from a 100 T-shapes shown in the image below (that consist of 4 squares)?"
        },
        {
          "type": "image_url",
          "image_url": {
            "url": "https://ujngnciigcpczmsloboz.supabase.co/storage/v1/object/public/quanta-public/internal-testing/tasks/drafts/images/1775307123018-tetramino.png"
          }
        }
      ],
      "is_context_required": false,
      "context_details": [],
      "correct_solutions": [
        {
          "slogan": "Construction with Repetition",
          "content": [
            {
              "type": "text",
              "text": "**Answer:** Yes, it is possible.\n\n**Explanation:** 1. From 4 of these pieces we can form a 4 x 4 square as shown in the image below:"
            },
            {
              "type": "image_url",
              "image_url": {
                "url": "https://ujngnciigcpczmsloboz.supabase.co/storage/v1/object/public/quanta-public/internal-testing/tasks/drafts/images/1775307441736-4_by_4_split.png"
              }
            },
            {
              "type": "text",
              "text": "2. And if we arrange these 4 x 4 squares into a 5 x 5 grid, we obtain a 20 x 20 square. The total number of tetraminos used is 4 * (5 * 5) = 100, as required."
            }
          ]
        }
      ]
    },
    "input_solution": [
      {
        "type": "text",
        "text": "First note that we can split the 20 x 20 square into 25 squares of 4 x 4. Secondly, note that we can combine 4 tetraminos into a 4 x 4 square as shown in the image below, and hence we can split the 20 x 20 square into 25 x 4 = 100 tetraminos!"
      },
      {
        "type": "image_url",
        "image_url": {
          "url": "https://ujngnciigcpczmsloboz.supabase.co/storage/v1/object/public/quanta-public/ai-model-test-images/example-4x4-tetromino-split.png"
        }
      }
    ],
    "reviewing_parameters": {
      "aux_rubric": {
        "validity": "-",
        "quality": "-"
      },
      "strictness": "lenient"
    }
  }'

Example 4: Hand-written Solution with Custom Rubric

A hand-written student solution submitted as an image, with a custom aux_rubric that enforces specific grading criteria (e.g. requiring proof of a particular claim for a top grade).

Hand-written Solution Review
curl -X POST https://api.quanta.world/api/review \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "task": {
      "content": [
        {
          "type": "text",
          "text": "Lazy Ania has divided the (closed) interval AB into 20 identical (closed) intervals and called each of them a **big** one. Whereas diligent Tony has divided that same interval into 2019 identical (closed) intervals and called each of them a **small** one. Let'\''s call a small interval **good** if it lies fully inside one of the big intervals. How many good intervals are there?"
        }
      ],
      "is_context_required": false,
      "context_details": [],
      "correct_solutions": [
        {
          "slogan": "Direct computation, nothing fancy",
          "content": [
            {
              "type": "text",
              "text": "**Answer**: 2000\n**Explanation**:\n1. A small interval won'\''t be good if it has at least one endpoint of one of the big intervals lying inside. But, any endpoint, except A and B, of each big interval lies inside exactly one of the small intervals. Therefore, there are 19 small intervals which are not good, so the answer is $2019-19=2000$.\n2. Remark: we relied on the fact that except A and B there are no endpoints of big intervals which coincide with endpoints of small intervals. We can see this as follows: if the $k$th endpoint of a big interval happens to be the $m$th endpoint of a small one, then $k \\cdot AB/20=m \\cdot AB/2019$, i.e. $2019k=20m$, but $20$ and $2019$ are co-prime, so $20$ divides $k$ and therefore $k \\geq 20$. However $k \\leq 20$, so that coinciding endpoint has to be B."
            }
          ]
        }
      ]
    },
    "input_solution": [
      {
        "type": "image_url",
        "image_url": {
          "url": "https://ujngnciigcpczmsloboz.supabase.co/storage/v1/object/public/quanta-public/ai-model-test-images/example-handwritten-solution.png"
        }
      }
    ],
    "reviewing_parameters": {
      "aux_rubric": {
        "validity": "It is important to prove that none of the endpoints of big and small intervals coincide. If this is not proven, the grade can be maximum B (if everything else is perfect).",
        "quality": "-"
      },
      "strictness": "auto",
      "cost_efficiency_mode": "standard"
    }
  }'

POST /api/bulk-review

Submit multiple student solutions for asynchronous AI review via CSV file upload. Requires the bulk_review permission. Unlike the single review endpoint, this returns immediately with a job ID. Poll GET /api/jobs/:jobId to track progress and retrieve results.

Request Format

Send a multipart/form-data request with the CSV file as the file field.

FieldTypeRequiredDescription
fileFile (CSV)YesCSV file containing review tasks. Max 10MB, up to 100 rows.

CSV Column Reference

ColumnRequiredFormatDescription
task.contentYesJSON arrayProblem statement as a PromptBlock[] JSON array
task.correct_solutionsYesJSON arrayReference solutions as a JSON array of solution objects
input_solutionYesJSON arrayStudent solution as a PromptBlock[] JSON array
task.is_context_requiredNoTRUE/FALSEWhether extra context is needed. Default: FALSE
task.context_detailsNoJSON arrayExtra context blocks (only used when is_context_required is TRUE)
reviewing_parameters.aux_rubricNoJSON objectGrading rubric with validity and quality fields
reviewing_parameters.strictnessNostringOne of auto, lenient, strict. Default: auto
reviewing_parameters.cost_efficiency_modeNostringModel tier per row. One of economy, standard, thorough. Default: standard

JSON values in cells are parsed automatically. LaTeX expressions with backslashes (e.g. \frac) are handled correctly.

Response

Returns immediately with a job ID. The reviews are processed asynchronously in the background.

json
{
  "success": true,
  "job": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "processing",
    "total_items": 2,
    "created_at": "2026-03-13T12:00:00.000Z"
  },
  "metadata": {
    "processingTimeMs": 150,
    "timestamp": "2026-03-13T12:00:00.150Z"
  }
}

Complete Example: Create CSV & Run Bulk Review

Full example showing how to programmatically create a CSV file with multiple review tasks, upload it for bulk review, poll until completion, and display results with grades and feedback.

Complete Bulk Review Example
# Step 1: Download the template CSV above and fill in your review tasks.
#
# Step 2: Upload CSV for bulk review
curl -X POST https://api.quanta.world/api/bulk-review \
  -H "Authorization: Bearer your-api-key" \
  -F "file=@bulk-review-template.csv"

# Step 3: Poll for job status (replace JOB_ID with the id from step 2)
curl https://api.quanta.world/api/jobs/JOB_ID \
  -H "Authorization: Bearer your-api-key"

# Repeat step 3 until job.status is "completed" or "failed".

GET /api/jobs/:jobId

Retrieve the status and results of a bulk review job. Use this to poll for progress after submitting a bulk review request. Requires a valid API key (any permission). You can only access jobs created with your own API key.

Path Parameters

ParameterTypeDescription
jobIdstring (UUID)The job ID returned from the bulk review endpoint

Response

json
{
  "success": true,
  "job": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "status": "completed",
    "total_items": 2,
    "processed_items": 2,
    "failed_items": 0,
    "total_bill_usd": 0.4127,
    "created_at": "2026-03-13T12:00:00.000Z",
    "started_at": "2026-03-13T12:00:00.150Z",
    "completed_at": "2026-03-13T12:01:05.000Z"
  },
  "results": [
    {
      "index": 0,
      "status": "success",
      "grade": "AA",
      "feedback_summary": "Excellent proof by contradiction...",
      "output": { "..." : "full review output" },
      "processing_time_ms": 24000,
      "error_message": null
    },
    {
      "index": 1,
      "status": "success",
      "grade": "BA",
      "feedback_summary": "Correct integration but...",
      "output": { "..." : "full review output" },
      "processing_time_ms": 18500,
      "error_message": null
    }
  ]
}

Job Status Values

StatusDescription
"pending"Job created but processing has not started yet
"processing"One or more reviews are currently being processed
"completed"All reviews have been processed (some may have failed)
"failed"The entire job failed due to a system error

Result Status Values

StatusDescription
"pending"Review has not been processed yet
"processing"Review is currently being processed by the AI
"success"Review completed — grade and feedback are available
"error"Review failed — see error_message for details
Example Request
curl https://api.quanta.world/api/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer your-api-key"

GET /api/jobs/:jobId/export

Download a finished bulk job's results as a CSV file instead of JSON. Same ownership rules as GET /api/jobs/:jobId — you can only export jobs created with your own API key. Useful for handing grades straight to a spreadsheet or gradebook.

Responds with text/csv and a Content-Disposition attachment header, named after the CSV you uploaded. One row per reviewed solution, carrying its grade, feedback summary and status. A job with no processed results yet returns 400, so poll GET /api/jobs/:jobId until at least one row is done before exporting.

Export Results as CSV
curl https://api.quanta.world/api/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890/export \
  -H "Authorization: Bearer your-api-key" \
  -o results.csv

GET /api/files

Read an image or file that one of your own reviews references. Review responses can carry storage references rather than public URLs, and this is the endpoint that resolves them. Requires a valid API key (any permission).

Query Parameters

ParameterTypeRequiredDescription
pathstringYesStorage path of the file, exactly as it appears in the reference
redirect1NoWhen 1, responds 307 to the file itself instead of returning JSON. This is the form that works directly as an <img src>.

Response

Without redirect=1 you get a short-lived signed URL, valid for 15 minutes:

json
{
  "signed_url": "https://…/storage/v1/object/sign/…?token=…"
}

Fetch the file promptly or re-request the reference; the URL is deliberately short-lived, so it should be resolved at read time rather than stored. Anything you may not read answers 404 — including files that exist but belong to someone else, so the endpoint never reveals whether another account's file exists.

Resolve a File Reference
# Follow the redirect straight to the file
curl -L "https://api.quanta.world/api/files?path=YOUR_FILE_PATH&redirect=1" \
  -H "Authorization: Bearer your-api-key" \
  -o image.png

HTTP Status Codes

CodeMeaning
200Request succeeded — review completed, job created, or job status returned
400Invalid request — malformed JSON, missing required fields, invalid CSV, or bad job ID format
401Missing, invalid, deactivated, or expired API key
402This API key is out of credits — top up to continue
403API key lacks the required permission, or the request references a file that does not belong to your account
404Resource not found (e.g. job ID does not exist)
429Daily review limit reached for this key — the count resets at midnight UTC
500Internal server error
503Service degraded — one or more health checks failed

Error Response Shape

Every non-2xx response carries the same two fields: error is a short, stable category and message is a human-readable explanation that may change. Never match on message.

json
{
  "error": "Rate limit exceeded",
  "message": "This API key has used 500 of its 500 reviews for today (UTC). The limit resets at midnight UTC. Contact us if you need a higher limit."
}

Bulk uploads that fail CSV validation add a details array naming the offending rows and columns:

json
{
  "error": "Invalid CSV",
  "message": "Failed to parse CSV file",
  "details": [
    "Row 3 — task.content: invalid JSON",
    "Row 7 — reviewing_parameters.strictness: \"harsh\" is not valid. Use one of: auto, lenient, strict"
  ]
}