// AccesPub API quickstart (Node.js 18+, no dependencies). // // One EPUB from start to finish: create a batch, upload, wait for the analysis, // list what needs human review, finalise and download the outputs. // // export ACCESPUB_KEY=ap_test_... # use a test key first: it uses no credits // node quickstart.mjs my-book.epub // // Docs: https://api.accespub.app/docs import { readFile, writeFile } from "node:fs/promises"; import { basename } from "node:path"; import { randomUUID } from "node:crypto"; const API = (process.env.ACCESPUB_API || "https://api.accespub.app/v1").replace(/\/$/, ""); const KEY = process.env.ACCESPUB_KEY; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function call(method, path, { body, idempotent = false, raw = false } = {}) { const headers = { Authorization: `Bearer ${KEY}` }; if (idempotent) headers["Idempotency-Key"] = randomUUID(); // safe to retry if (body && !(body instanceof FormData)) { headers["Content-Type"] = "application/json"; body = JSON.stringify(body); } let res = await fetch(API + path, { method, headers, body }); if (res.status === 429) { await sleep(1000 * Number(res.headers.get("Retry-After") || 5)); res = await fetch(API + path, { method, headers, body }); } if (!res.ok) { // Errors are RFC 9457 problem documents: {type, title, status, detail} const p = await res.json().catch(() => ({})); throw new Error(`${method} ${path} → ${res.status} ${p.title || ""}: ${p.detail || ""}`); } return raw ? res : res.json(); } async function main() { const file = process.argv[2]; if (!KEY || !file) { console.error("Usage: ACCESPUB_KEY=ap_test_... node quickstart.mjs my-book.epub"); process.exit(1); } const org = await call("GET", "/organization"); console.log(`${org.name} · mode ${org.mode} · credits available ${org.credits.available}`); const batch = await call("POST", "/batches", { idempotent: true, body: { name: "Quickstart (Node)" } }); const form = new FormData(); form.append("file", new Blob([await readFile(file)], { type: "application/epub+zip" }), basename(file)); let job = await call("POST", `/batches/${batch.id}/jobs`, { idempotent: true, body: form }); console.log(`Uploaded ${job.filename} → job ${job.id}`); while (["queued", "processing"].includes(job.status)) { await sleep(10000); job = await call("GET", `/jobs/${job.id}`); console.log(` status: ${job.status}`); } if (job.status === "failed") throw new Error(`Processing failed: ${JSON.stringify(job.error)}`); const b = job.billing; console.log(`${b.images} images → ${b.credits_required} credit(s) on finalisation`); let cursor = ""; do { // lists are paginated: up to 100 items per page, follow next_cursor const page = await call("GET", `/jobs/${job.id}/review-items?status=pending&limit=100${cursor ? `&cursor=${cursor}` : ""}`); for (const it of page.data) console.log(` pending ${it.kind} ${it.id} (${it.document})`); cursor = page.next_cursor || ""; } while (cursor); // Here your own front end would show each item to a reviewer and send // POST /jobs/{id}/decisions — see section 4 of the integration guide. const allowPending = job.review.pending > 0; // without review: no conformance declared await call("POST", `/jobs/${job.id}/finalize`, { idempotent: true, body: { allow_pending: allowPending } }); while (job.status !== "completed") { await sleep(5000); job = await call("GET", `/jobs/${job.id}`); if (job.error && job.status !== "finalizing") throw new Error(`Finalisation failed: ${JSON.stringify(job.error)}`); } console.log(`Completed · conformance: ${job.conformance}`); const stem = basename(file).replace(/\.epub$/i, ""); for (const [kind, name] of [["epub", `${stem}-accessible.epub`], ["report_html", `${stem}-report.html`], ["onix_a11y", `${stem}-onix.xml`]]) { const res = await call("GET", `/jobs/${job.id}/files/${kind}`, { raw: true }); await writeFile(name, Buffer.from(await res.arrayBuffer())); console.log(` saved ${name}`); } } main().catch((e) => { console.error(e.message); process.exit(1); });