SchoolDigger API
  • Plans
  • Documentation
  • Change Log
  • Data Privacy
  • Sign in

Batch Requests

Submit up to 2,000 API calls at once, then download the results when they are ready

In short

  • POST https://api.schooldigger.com/batches with a JSON list of API paths. You get a batch id back right away.
  • SchoolDigger runs the calls for you, paced at half your plan's per-minute limit, so the batch leaves the other half for your live traffic.
  • Poll the batch status, then download the results in parts of 20 as each part finishes.
  • Each call counts as one API call against your plan, as if you made it yourself (a call retried after a server error may count twice). Submitting, polling and downloading are free.
  • Available on paid plans (not Developer or DEMO). Use it for more than 100 calls, or for any job you don't want to pace yourself.

1. Submit a batch

POST /batches?appID=YOUR_APP_ID&appKey=YOUR_APP_KEY with Content-Type: application/json:

{
  "items": [
    { "id": "a1", "path": "/v3.1/schools/064215006903" },
    { "id": "a2", "path": "/v3.1/districts/0622710" },
    { "id": "a3", "path": "/v3.1/schools?st=CA&q=oak&page=2" }
  ],
  "callbackUrl": "https://example.com/hooks/sd-batch"
}
  • path: any GET path on the API, starting with /v2. or /v3.. Leave out appID and appKey: the batch adds yours.
  • id: your own key for the item, 1-64 characters, unique in the batch. Optional; it defaults to the item's position ("0", "1", …). Ids are always returned as strings.
  • callbackUrl: optional, https only. See Callback.

The response is 202 Accepted, with the status URL in the Location header:

{
  "batchId": "b_7f3c2a9e1d4b",
  "status": "queued",
  "itemCount": 3,
  "partSize": 20,
  "partCount": 1,
  "estimatedSeconds": 4,
  "statusUrl": "https://api.schooldigger.com/batches/b_7f3c2a9e1d4b"
}

If any item is invalid, the whole batch is rejected with 400 and an errors array listing up to 10 bad items, so nothing runs half-valid. A batch also has to fit in your remaining daily or monthly quota when it is submitted.

2. Check status

GET /batches/{batchId}?appID=...&appKey=...

{
  "batchId": "b_7f3c2a9e1d4b",
  "status": "running",
  "itemCount": 500,
  "completedCount": 220,
  "failedCount": 3,
  "partSize": 20,
  "partCount": 25,
  "partsReady": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
  "submittedUtc": "2026-10-27T18:02:11Z",
  "startedUtc": "2026-10-27T18:02:14Z",
  "completedUtc": null,
  "pausedUntilUtc": null,
  "estimatedSecondsRemaining": 336,
  "expiresUtc": null
}
  • status: queued, running, completed, canceled, failed (could not run at all) or expired (results deleted after 72 hours).
  • failedCount: items whose final status is not 2xx (a 404 for an unknown school, for example). The batch is still completed; each item's result has the detail.
  • partsReady: parts you can download now, even while the batch is still running.
  • While the batch is queued or running, a Retry-After header says how many seconds to wait before polling again.
  • pausedUntilUtc is set while the batch waits out an API outage or a used-up daily or monthly limit. It resumes on its own.
  • Only the appID that submitted a batch can see it.

3. Download results

GET /batches/{batchId}/results?part=N&appID=...&appKey=..., for N from 0 to partCount - 1. Each part holds 20 items in the order you submitted them, as gzipped NDJSON (one JSON object per line):

{"id":"a1","path":"/v3.1/schools/064215006903","status":200,"body":{ ...the API response... }}
{"id":"a2","path":"/v3.1/districts/9999999","status":404,"body":{"code":404,"message":"...","description":"...","help":"..."}}
  • body is the same JSON the call returns on its own, with the same plan fields, written on one line.
  • The part is sent with Content-Encoding: gzip. Most HTTP clients unzip it for you; with curl add --compressed.
  • A part that isn't ready yet returns 409 with the current status. After the 72-hour retention, results return 410.

Cancel

DELETE /batches/{batchId}?appID=...&appKey=... stops a queued or running batch and returns its status. Calls already made still count, and their parts stay downloadable.

Limits and pacing

Items per batchUp to 2,000
Items per result part20 (a full batch has 100 parts)
Batches per appID3 queued or running at once; they run one at a time
Results kept72 hours after the batch finishes
PlansPaid plans. Developer and DEMO keys get 403.

A batch uses at most half of your plan's per-minute limit, spread evenly, and slows down further if your own traffic is using more than its half. It starts within about a minute of submit. Typical run times:

Plan limitBatch rate500 calls2,000 calls
100 calls/min50/min10 min40 min
500 calls/min250/min2 min8 min

Temporary API errors (timeouts, 5xx, 429) are retried for you. An item that still fails is recorded with its last status code, or 599 with an error message for a network failure.

If your app restricts referrers or IPs, the batch's calls carry the Referer you sent when you submitted (or, without one, your IP address), so the same restrictions pass.

Callback

With a callbackUrl, SchoolDigger POSTs the status JSON there when the batch is completed, canceled or failed, and retries after 1, 5 and 30 minutes if your server doesn't answer with 2xx. Polling still works either way.

The X-SchoolDigger-Signature header is sha256= followed by the hex HMAC-SHA256 of the raw body. The HMAC key is the lowercase hex SHA-256 of your appKey. To check it in Python:

import hashlib, hmac

def valid(raw_body: bytes, header: str, app_key: str) -> bool:
    key = hashlib.sha256(app_key.encode()).hexdigest().encode()
    expected = "sha256=" + hmac.new(key, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Errors

Errors use the API's usual shape, {"code","message","description","help"}.

CodeWhen
400Invalid batch (see errors), or a part out of range
401Missing or invalid appID / appKey
403The plan can't submit batches (Developer, DEMO)
404No such batch for this appID
409The part isn't ready yet
410The results were deleted after 72 hours
413The request body is too large
429Not enough daily or monthly quota left for the batch, or 3 batches already queued or running
503The key couldn't be checked right now; try again in a minute

Examples

curl

CREDS="appID=YOUR_APP_ID&appKey=YOUR_APP_KEY"

curl -X POST "https://api.schooldigger.com/batches?$CREDS" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"id":"a1","path":"/v3.1/schools/064215006903"},{"id":"a2","path":"/v3.1/districts/0622710"}]}'

curl "https://api.schooldigger.com/batches/b_7f3c2a9e1d4b?$CREDS"

curl --compressed "https://api.schooldigger.com/batches/b_7f3c2a9e1d4b/results?part=0&$CREDS"

Python

import json, time, requests

BASE = "https://api.schooldigger.com"
CREDS = {"appID": "YOUR_APP_ID", "appKey": "YOUR_APP_KEY"}

items = [{"id": nces, "path": f"/v3.1/schools/{nces}"}
         for nces in ["064215006903", "341341003182", "360007702877"]]

batch = requests.post(f"{BASE}/batches", params=CREDS, json={"items": items}).json()

while True:
    r = requests.get(f"{BASE}/batches/{batch['batchId']}", params=CREDS)
    status = r.json()
    if status["status"] not in ("queued", "running"):
        break
    time.sleep(int(r.headers.get("Retry-After", 30)))

for part in status["partsReady"]:
    r = requests.get(f"{BASE}/batches/{batch['batchId']}/results",
                     params={**CREDS, "part": part})
    for line in r.text.splitlines():
        result = json.loads(line)
        print(result["id"], result["status"])

JavaScript (Node.js 18+)

const BASE = "https://api.schooldigger.com";
const CREDS = "appID=YOUR_APP_ID&appKey=YOUR_APP_KEY";

const items = ["064215006903", "341341003182", "360007702877"]
  .map(id => ({ id, path: `/v3.1/schools/${id}` }));

const batch = await (await fetch(`${BASE}/batches?${CREDS}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ items })
})).json();

let status;
while (true) {
  const r = await fetch(`${BASE}/batches/${batch.batchId}?${CREDS}`);
  status = await r.json();
  if (!["queued", "running"].includes(status.status)) break;
  const wait = Number(r.headers.get("Retry-After") || 30);
  await new Promise(res => setTimeout(res, wait * 1000));
}

for (const part of status.partsReady) {
  const r = await fetch(`${BASE}/batches/${batch.batchId}/results?part=${part}&${CREDS}`);
  for (const line of (await r.text()).split("\n").filter(Boolean)) {
    const result = JSON.parse(line);
    console.log(result.id, result.status);
  }
}
The older /batch endpoint (one multipart/mixed request that waits for every call) still works but is deprecated, and is limited to 100 calls. New code should use /batches.

See the API reference and the Change Log. For a large part of the database, a bulk data download may be a better fit. Questions: api@schooldigger.com.

  • © 2026 SchoolDigger.com,
  • Powered by 3scale