Jobs, polling, webhooks and streaming
How long work stays safe: 202 straight away, then poll, listen for a webhook or stream progress.
Why everything is a job
Marking an essay or building a course can take minutes. Instead of holding your connection open, every operation that can take more than two seconds returns 202 and a job at once. Your request never times out and a dropped connection loses nothing.
- Queued202 and a job id, straight away
- ProcessingPoll, stream or wait for a webhook
- SucceededResult and files on the job
| Status | Meaning | Charged |
|---|---|---|
queued | Accepted and waiting for a worker. | Reserved |
processing | Running. progress shows the stage. | Reserved |
succeeded | Finished. result and files are ready. | Yes |
needs_context | Finished, but something needed was missing. result.reason says what. | No |
failed | Could not finish. error explains why. | No |
cancelled | You cancelled it. | No |
expired | The result was deleted after the retention period. | — |
Polling (the main path)
Call GET /v1/jobs/{id}. Wait at least poll_after_seconds (or the Retry-After header) between calls; the value grows for long jobs. Polling is free.
async function waitForJob(id) {
for (;;) {
const response = await fetch(`https://api.edumark.ai/v1/jobs/${id}`, { headers: { Authorization: `Bearer ${process.env.EDUMARK_API_KEY}` } });
const job = await response.json();
if (!['queued', 'processing'].includes(job.status)) return job;
const wait = Number(response.headers.get('Retry-After')) || job.poll_after_seconds;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}Webhooks
Rather than polling, add an HTTPS endpoint in the console and EduMark tells you the moment a job finishes with a signed job.completed notice. See webhooks for the payload, signatures and retries.
Streaming progress
For a live progress bar, open GET /v1/jobs/{id}/events. It is a server-sent event stream with progress, status and done events and a heartbeat every 15 seconds. Each stream closes after 25 seconds; reconnect with the Last-Event-ID header and you carry on where you left off. Keep polling as your source of truth.
const response = await fetch(`https://api.edumark.ai/v1/jobs/${id}/events`, {
headers: { Authorization: `Bearer ${process.env.EDUMARK_API_KEY}`, Accept: 'text/event-stream' },
});
const decoder = new TextDecoder();
for await (const chunk of response.body) {
for (const line of decoder.decode(chunk, { stream: true }).split('\n')) {
if (line.startsWith('data: ')) render(JSON.parse(line.slice(6)));
}
}
// The stream closes after 25 s: reconnect with Last-Event-ID, or fall back to polling.Cancel and delete
POST /v1/jobs/{id}/cancel stops a queued or running job and releases what was reserved. DELETE /v1/jobs/{id} deletes the request, result and files straight away; otherwise they are deleted after 30 days.