AccesPub API integration guide
Version 0.10 (1 November 2026). Everything in this guide is available at
https://api.accespub.app/v1, webhooks included (§6). The full reference is at /docs/referencia and the contract at openapi.yaml.
The API does in batches what the AccesPub dashboard does with one EPUB: it analyses, fixes everything that can be fixed automatically, leaves what needs judgement to a person, and delivers the final EPUB with its report and its accessibility ONIX.
There are two ways of working with AccesPub:
- Portal only, with no coding: everything happens in the AccesPub portal (api.accespub.app/portal), with its own help in four languages.
- Mixed model, to integrate it with your systems: your system does the mechanical work through the API (creating batches, uploading EPUBs, sending metadata and commercial data, downloading the outputs) and human review happens in the portal, which your front end links to (see “Mixed model”).
The portal uses this same API: it has no features the API lacks. If you also need to run the review in your own front end, talk to ab serveis first.
1. Getting started
- Ask ab serveis for an account. They will set up your organisation and invite your first administrator.
- Create the keys you need in the portal (Organisation → API keys) or with
POST /api-keys("mode": "test"for testing). A test key starts withap_test_and is shown only once: keep it in your secrets manager, never in your code. - Try the key:
export ACCESPUB_KEY=ap_test_...
curl -s https://api.accespub.app/v1/organization \
-H "Authorization: Bearer $ACCESPUB_KEY"
ap_test_ keys use no credits and their data is kept separate from live data.
For production, create an ap_live_ key.
One EPUB from start to finish
There is a complete, tested example in Python
(with interactive review in the terminal) and in JavaScript for
Node.js. Step by step with curl:
# 1. Create a batch
curl -s https://api.accespub.app/v1/batches \
-H "Authorization: Bearer $ACCESPUB_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "Test", "on_pending": "wait"}'
# → {"id": "bat_01JAB2C3D4", "status": "open", ...}
# 2. Upload an EPUB (one per request)
curl -s https://api.accespub.app/v1/batches/bat_01JAB2C3D4/jobs \
-H "Authorization: Bearer $ACCESPUB_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-F "file=@the-travelling-water.epub" \
-F "external_id=SKU-1234"
# → 202 {"id": "job_01JAB2C9XY", "status": "queued", ...}
# 3. Wait for the analysis (poll the status every 10-30 s)
curl -s https://api.accespub.app/v1/jobs/job_01JAB2C9XY \
-H "Authorization: Bearer $ACCESPUB_KEY"
# → "status": "needs_review", "review": {"total": 22, "pending": 22}
# 4. Review (see §4) and 5. finalise (or create the batch with "auto_finalize": true)
curl -s -X POST https://api.accespub.app/v1/jobs/job_01JAB2C9XY/finalize \
-H "Authorization: Bearer $ACCESPUB_KEY" \
-H "Idempotency-Key: $(uuidgen)"
# 6. Download the final EPUB
curl -s -o final.epub \
https://api.accespub.app/v1/jobs/job_01JAB2C9XY/files/epub \
-H "Authorization: Bearer $ACCESPUB_KEY"
People and roles
Everyone who reviews has their own account. An administrator invites them:
curl -s https://api.accespub.app/v1/invitations \
-H "Authorization: Bearer $ACCESPUB_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"email": "laura@publisher.example", "role": "reviewer", "name": "Laura Pons"}'
# → {"id": "usr_…", "status": "invited", "access_url": "https://…/invitacion?token=…", "email_sent": true}
| Role | What it can do |
|---|---|
admin |
Everything, including people and keys |
batch_manager |
Batches, uploads, review, finalising and downloads |
reviewer |
View, review and download (cannot create batches or finalise) |
external_reviewer |
Review only the batch they are invited to (batch_id), without a password: they sign in with their link, valid for 30 days |
- The link is emailed and also returned to you in
access_url(it is personal). The person sets their password withPOST /invitations/accept. - API keys remain the way to integrate systems: an administration key acts as
adminand the others asbatch_manager. - If you build your own front end with personal sessions:
POST /sessionsreturns a cookie and acsrf_token, which must be sent in theX-CSRF-Tokenheader on requests that change something; useAccesPub-Mode: testfor test data. This is exactly what the portal uses. - Personal sessions (portal or
POST /sessions) are renewed with activity: they expire after 12 hours without use and, at most, 7 days after they started. API keys have no session: each request carries the key and that is enough. - Deactivating a person (
DELETE /users/{id}) closes their sessions; their name stays on the decisions they signed.
Mixed model: your system and the portal
Your system orchestrates through the API and people review in the AccesPub portal, which already handles everything that needs judgement (each image with its context, AI suggestions that are never applied on their own, tables, languages and the signature of every decision).
- Create the batch with
"on_pending": "wait","auto_finalize": true(each title is finalised on its own when its last decision is made in the portal) and, if you want the AI suggestions ready,"defaults": {"ai_prefill": true}. - Register your reviewers with
POST /invitations(with an administration key). If they will always come from your application, add"send_email": false: they do not get the “create your password” email and do not need one (see step 4). - Upload the EPUBs and send metadata and commercial data from your catalogue
(
PATCH …/metadata,PATCH …/commercial). - When a title reaches
needs_review, show a “Review in AccesPub” button in your front end. Each title hasportal_url(its page in the portal) andreview_url(its review screen); each batch, itsportal_url. Test titles already include?mode=test. When it is clicked, your server requests a sign-in link for that person and redirects them: they reach the review with no password (see below). Without a sign-in link, the button opensreview_urland the person signs in. - When the review is done, the title is finalised on its own; your system gets
the
job.completednotification (webhooks, §6) and downloads the outputs. Without webhooks, pollGET /jobs/{id}until you seecompleted.
With webhooks (§6), your system does not need to poll: it receives
job.needs_review (to show the “Review in AccesPub” button), job.completed and
batch.completed.
curl -s https://api.accespub.app/v1/jobs/job_01JAB2C9XY \
-H "Authorization: Bearer $ACCESPUB_KEY"
# → "status": "needs_review",
# "portal_url": "https://api.accespub.app/portal/titulos/job_01JAB2C9XY",
# "review_url": "https://api.accespub.app/portal/titulos/job_01JAB2C9XY/revision"
Signing in to the portal without signing in again
Your application already knows who the person is (it has its own sign-in). With a sign-in link, AccesPub opens the portal with that person's session without asking for a password:
# On your server, when "Review in AccesPub" is clicked (usr_… = the person in AccesPub)
curl -s https://api.accespub.app/v1/users/usr_01JAB2D4EF/sign-in-link \
-H "Authorization: Bearer $ACCESPUB_KEY" -H "Content-Type: application/json" \
-d '{"next": "https://api.accespub.app/portal/titulos/job_01JAB2C9XY/revision"}'
# → 201 {"url": "https://api.accespub.app/portal/entrar?token=…",
# "expires_at": "…", "user_id": "usr_01JAB2D4EF", "next": "/portal/titulos/job_01JAB2C9XY/revision"}
# Redirect the person's browser to "url".
- It has to be enabled for your organisation (ab serveis enables it): AccesPub
relies on your sign-in to identify your people. If it is not enabled, the response is
403 sign-in-links-disabled. - Single use and 5 minutes: request it at the moment of the click, always from your server (the API key never goes to the browser).
- Decisions are signed by that person. A key that is not an administration key cannot request links for an administrator.
-
Keep each person's AccesPub
idin your system (POST /invitationsreturns it andGET /userslists them). -
Open the portal in a new tab or window: it cannot be embedded in another site (for security it does not allow frames or
iframe). - Reviewers need an account in your organisation. If they are not signed in, the portal asks them to sign in and takes them back to the same screen.
- One-off review by someone external (for example, a proofreader) is done with an
external reviewer (
external_reviewer), who signs in with the link to their batch.
2. Job lifecycle
Each uploaded EPUB is a job within a batch.
Usual path: queued → processing → needs_review → ready → finalizing →
completed. If there is nothing to review, it goes straight from processing to
ready. A new decision on a completed title sends it back to ready, and
POST /jobs/{id}/reprocess puts it back in the queue.
| Status | What it means | What to do |
|---|---|---|
queued |
Queued | Wait |
processing |
Analysing and fixing | Wait |
needs_review |
A person must make decisions | Review (§4) |
ready |
Nothing pending | Finalise |
finalizing |
Generating the final EPUB | Wait |
completed |
Outputs available | Download (§5) |
failed |
Could not be processed; see error |
Fix the EPUB and upload it again |
The batch has an aggregate status (GET /batches/{id}) and counters per status.
3. Batches and limits
- One EPUB per request. If an upload fails, retry only that file.
- Default limits: 50 MB per EPUB, 50 EPUBs and 1 GB per batch, 5 GB of
storage per organisation. See
GET /organization(limits,storage). - EPUBs over 50 MB: before uploading, request a large upload
(
POST /large-upload-requests). It is usually caused by unoptimised images or embedded fonts or video. If ab serveis approves it, the batch accepts that file, up to 200 MB, for 7 days. - Fixed-layout EPUBs are not accepted (
422 fixed-layout-not-supported). external_idstores your identifier for the title and is returned in every response, webhook and in the manifest.
4. Human review
AccesPub does not declare WCAG 2.1 AA conformance unless a person has reviewed what requires judgement: image alternative text, tables and languages.
- Get the pending items:
GET /jobs/{id}/review-items?status=pending(paginated: up to 100 per page, follownext_cursor). Each item includes its context in the book (text before and after) and, for images, a URL to display it (…/image, scaled to 1024 px). There are three kinds:
| Kind | What is decided | Decision |
|---|---|---|
image |
Alternative text, or that it is decorative | {"kind": "image", "decorative": false, "alt": "…"} |
table |
Whether it is a data table (with caption) or a layout table | {"kind": "table", "layout": false, "caption": "…", "first_row_as_headers": true} |
language, scope: inline |
Keep or remove an xml:lang in the text |
{"kind": "language", "action": "keep"} or "remove" |
language, scope: document |
The language of a document | {"kind": "language", "action": "keep"} or "replace" with "new_lang": "ca" |
language, scope: book (id: book_language) |
The book's main language and its order in the OPF | {"kind": "language", "action": "replace", "languages": ["es", "en"]} (main language first) or "keep" |
The book language appears as pending when the OPF declares several languages,
when its language does not match the text, or when it has no valid one
(language.reasons). It includes the declared languages (declared), a
proposal (proposed) and the ones that can be chosen (candidates). If there
is nothing to confirm, it arrives already decided and you can still change it.
Some document languages arrive already decided (decision.auto: true):
AccesPub assigned them with high confidence. You can change them like any
other.
2. Optionally, ask the AI for a suggestion (images and tables):
POST /jobs/{id}/review-items/{item_id}/suggestion. It is written in the item's
language. The suggestion is never applied automatically: show it to the
person so they accept or change it, and send it with "from_suggestion": true.
With ap_test_ keys the suggestion is a fixed test text (no cost). There is a
per-minute limit (limits.ai_suggestions_per_minute).
Bulk suggestions (0.7; tables since 0.11). To avoid asking one by one, create the batch with
"defaults": {"ai_prefill": true} or call POST /jobs/{id}/ai-prefill: the AI
prepares a suggestion for every pending image in the background (except SVGs) and a
caption for every pending table (images and tables in the status; tables do not
count towards paid blocks).
GET /jobs/{id}/ai-prefill returns the progress (status: running, done…;
done, queued, failed). Each suggestion appears in review-items as if it had
been requested by hand and is not applied automatically either: the person
validates it, corrects it or marks the image as decorative. The queue shares the
pace between organisations (suggestions requested by hand go first) and has a
daily cap (limits.ai_prefill_per_day). Depending on your contract it is included
in the title credit (you need enough balance to finalise it) or charged separately,
per block of images and when requested (anything not generated is refunded);
billing in the status says which. Each image also carries hint: whether it looks
informative (figure caption, cover) or decorative (rule, spacer…), with the reasons,
and the group of near-identical images (chapter headers, for example) so they can be
validated together.
3. Send the decisions with the person who makes them (up to 200 per request):
curl -s https://api.accespub.app/v1/jobs/job_01JAB2C9XY/decisions \
-H "Authorization: Bearer $ACCESPUB_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"reviewer": {"name": "Laura Pons", "email": "laura@publisher.example"},
"decisions": [
{"item_id": "img_0004", "decision": {"kind": "image", "decorative": false,
"alt": "Diagram of the water cycle: evaporation, condensation and rain.",
"from_suggestion": true}},
{"item_id": "img_0005", "decision": {"kind": "image", "decorative": true}},
{"item_id": "9f3c…", "decision": {"kind": "table", "layout": false,
"caption": "Average temperatures by month", "first_row_as_headers": true}},
{"item_id": "lang_0001", "decision": {"kind": "language", "action": "keep"}},
{"item_id": "dlang_0002", "decision": {"kind": "language", "action": "replace",
"new_lang": "ca"}}
]
}'
# → {"saved": 5, "review": {"total": 22, "decided": 9, "pending": 13},
# "job_status": "needs_review"}
- All or nothing. If any decision is invalid, the response is
422 invalid-decisionwith one error per decision inerrors("pointer": "/decisions/3/decision/alt") and none is saved. Fix them and resend the whole request. - Each decision is saved immediately with reviewer and date. The review can be shared between several people and take weeks.
- Sending another decision for the same item replaces it; the history is kept
(
GET /jobs/{id}/decisions).review-itemsshows indecided_byanddecided_atwho made the current decision. - If you reprocess a title, the decisions are applied again automatically.
- Who signs. With a person's session, the reviewer is that person. With an
API key,
revieweris required and its email must be that of an active person in your organisation (or an external reviewer of that batch); otherwise422 reviewer-not-registered. Register everyone who reviews (see "People and roles"). - Your organisation is responsible for the identity and competence of its
reviewers (as stated in the contract). AccesPub flags in
review_flags, without blocking, unusual patterns:fast_decisions(10 or more decisions at less than 3 seconds per item since the end of the analysis) andall_ai_suggestions_accepted(5 or more AI suggestions, all accepted unchanged). - If you do not want to review: finalise with
allow_pending: true(or create the batch withon_pending: finalize). The EPUB is delivered with all automatic fixes, but without declaring conformance, and the report lists what is pending. - Skip the review (as in the dashboard):
POST /jobs/{id}/skip-reviewmarks the pending images, tables and inline languages as decorative, layout or kept. With"confirm_decorative": trueandreviewer, a person confirms that this is correct: they become decisions with a reviewer and count towards conformance. Without confirmation, the EPUB is delivered the same way but without conformance.
Completing metadata
The same corrections that are made by hand on the dashboard's export screen:
curl -s -X PATCH https://api.accespub.app/v1/jobs/job_01JAB2C9XY/metadata \
-H "Authorization: Bearer $ACCESPUB_KEY" -H "Content-Type: application/json" \
-d '{"dc": {"title": "The Travelling Water", "creator": "Laura Pons", "date": "2020-06-05"},
"page_source_isbn": "9788400000001",
"accessibility_summary": "The publisher’s own text…"}'
GET /jobs/{id}/metadatashows each field with its origin (epuboroverride) and the notices (notices), which also appear on the job:date_ambiguous(the EPUB date cannot be interpreted),page_source_missing(there is a page list without the print edition it comes from) andtitle_placeholder(the EPUB had no title).- Notices do not block, as in the dashboard, but they are worth resolving.
nullremoves a value and restores the EPUB's. Changes are applied on finalisation and are kept if you reprocess the title.
Subjects and AI suggestion
subjects replaces all the EPUB's subjects. Thema and BISAC are written to the OPF
with their EPUB 3 refinement (authority + term) and go into the accessibility
ONIX together with the keywords:
curl -s -X PATCH https://api.accespub.app/v1/jobs/job_01JAB2C9XY/metadata \
-H "Authorization: Bearer $ACCESPUB_KEY" -H "Content-Type: application/json" \
-d '{"dc": {"description": "A novel about three generations of a family from Valencia…"},
"subjects": {"thema": ["FBA", "1DSE"], "bisac": ["FIC019000"],
"keywords": ["family", "memory", "Valencia"]}}'
If the description or subjects are missing, POST /jobs/{id}/metadata/suggestion
returns an AI suggestion (synopsis, Thema codes checked against the official list,
a BISAC hint in words and keywords) based on about 4,000 characters of the book.
It is never applied on its own and does not use credits: send with PATCH what a
person accepts. With an ap_test_ key the suggestion is fixed and the AI is not called.
Commercial ONIX
Besides the accessibility ONIX, the API generates the full ONIX 3.1 of each title, with the commercial data that are not in the EPUB: prices per market, publishing status and date, imprint, series, authors with their role, audience…
# Common batch values (also when creating it, in defaults.commercial)
curl -s -X PATCH https://api.accespub.app/v1/batches/bat_01JAB2C3D4/commercial \
-H "Authorization: Bearer $ACCESPUB_KEY" -H "Content-Type: application/json" \
-d '{"pub_status": "04", "supplier_name": "De Marque",
"markets": [{"territory": "CA", "currency": "CAD", "price_type": "01"},
{"territory": "FR BE CH", "currency": "EUR", "price_type": "04", "tax_percent": "5.5"}]}'
# Title data (the title takes precedence; the title's markets replace the batch ones)
curl -s -X PATCH https://api.accespub.app/v1/jobs/job_01JAB2C9XY/commercial \
-H "Authorization: Bearer $ACCESPUB_KEY" -H "Content-Type: application/json" \
-d '{"subtitle": "Un essai", "publishing_date": "2026-11-15",
"markets": [{"territory": "CA", "currency": "CAD", "price_type": "01", "price_amount": "24.95"},
{"territory": "FR BE CH", "currency": "EUR", "price_type": "04", "tax_percent": "5.5", "price_amount": "14.99"}]}'
# Check, generate (may use credits, once per title) and download
curl -s https://api.accespub.app/v1/jobs/job_01JAB2C9XY/commercial -H "Authorization: Bearer $ACCESPUB_KEY"
curl -s -X POST https://api.accespub.app/v1/jobs/job_01JAB2C9XY/onix-full -H "Authorization: Bearer $ACCESPUB_KEY"
curl -s -o onix.xml "https://api.accespub.app/v1/jobs/job_01JAB2C9XY/files/onix_full?naming=isbn" \
-H "Authorization: Bearer $ACCESPUB_KEY"
- A market is a territory (ISO countries such as
ESorFR BE CH, or ONIX regions such asWORLD) with its currency, price, price type, tax, availability, supplier and on-sale date. Several currencies in one territory: repeat the territory. GET /jobs/{id}/commercialreturns the effective values, where each one comes from and the checks (issues); with anyerror,POST /onix-fullreturns422.apply_to_batchin the title PATCH copies common fields to the batch (markets without prices).- Changing the data later does not require finalising again and is not charged
again: each download is built with the current data. For the whole batch:
POST /batches/{id}/onix-full(all or nothing) andGET /batches/{id}/files/onix_full. - The cost is set by the organisation's contract (
billingin the GET).ap_test_keys use no credits.
Finalising
Finalising generates the final EPUB and uses its credits (section 8). By default
it is an explicit step (POST /jobs/{id}/finalize, or POST /batches/{id}/finalize
for all ready titles), so you decide when credits are spent and can check before.
If you prefer it to happen automatically, create the batch with
"auto_finalize": true: each title is finalised when its last decision is made.
Exception: titles that use more than one credit are not finalised automatically
until someone confirms the notice in the portal or you call /finalize
explicitly.
If you change a decision on a title that was already finalised, it goes back to
ready and must be finalised again (at no cost).
5. Outputs
| Output | Per title | Per batch |
|---|---|---|
| Final EPUB | GET /jobs/{id}/files/epub |
GET /batches/{id}/files/epubs (.zip) |
| HTML report | GET /jobs/{id}/files/report_html |
GET /batches/{id}/files/reports (.zip) |
| JSON report | GET /jobs/{id}/report |
— |
| Accessibility ONIX 3.1 | GET /jobs/{id}/files/onix_a11y |
GET /batches/{id}/files/onix_a11y (one message with one <Product> per title) |
| Commercial (full) ONIX 3.1 | GET /jobs/{id}/files/onix_full |
GET /batches/{id}/files/onix_full (titles with the commercial ONIX generated) |
| Manifest | — | GET /batches/{id}/manifest (JSON, or CSV with ?format=csv or Accept: text/csv) |
The manifest gives, for each title, the status of each output (ready,
ready_with_warnings, expired…) and what is missing (for example, the ISBN for
the ONIX). The HTML report is in Spanish by default; use ?lang=en, ca or fr
for another language. An incomplete title does not block the batch: whatever is
ready is delivered.
Final files are kept for 30 days after finalisation. Decisions and reports are kept longer, so a title can be reprocessed without reviewing it again.
Nothing under review expires. If a live batch with titles to review or finalise
has had no activity for 30 days, AccesPub emails a reminder to the people with
the admin and batch_manager roles in the organisation; after 60 days, a final
one. The maximum time to complete a batch is set in the contract.
6. Webhooks
AccesPub notifies your system when something changes, at a URL of yours (https):
no need to poll GET /jobs/{id}. Addresses are added with an administration key
(POST /webhook-endpoints) or in the portal (Organisation → Automatic
notifications). Each mode has its own: those of an ap_test_ key only receive
events about test data.
curl -s https://api.accespub.app/v1/webhook-endpoints \
-H "Authorization: Bearer $ACCESPUB_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://erp.velablanca.example/accespub/notifications",
"events": ["job.needs_review", "job.completed", "batch.completed", "credits.low"]}'
# → 201 {"id": "whe_…", "secret": "whsec_…", …} ← store the secret: it is only shown now
| Event | When | data |
|---|---|---|
job.needs_review |
The analysis has finished and there are items to review | Job |
job.ready |
Nothing is pending any more: ready to finalise | Job |
job.completed |
A title has been finalised and its outputs are ready | Job |
job.failed |
It could not be analysed (status: failed) or finalised (it returns to its status, with error) |
Job |
job.ai_prefill_done |
The title’s AI proposals have finished (images and tables) | Job with ai_prefill |
batch.completed |
All titles in the batch are finalised or failed | Batch |
large_upload_request.resolved |
ab serveis has approved or rejected a large upload | LargeUploadRequest |
support_request.answered |
ab serveis has answered a help request | SupportRequest |
credits.low |
The balance drops below the organisation’s threshold (10 by default) and again when it runs out; live only |
CreditsLow |
Without events, the address receives all of them. A title finalised
automatically (auto_finalize) sends job.completed, not job.ready.
Each notification is a POST with this body and the webhook-id,
webhook-timestamp and webhook-signature headers (Standard Webhooks):
{"id": "evt_01JC5YB8Q0VZ3K2M4N6P8R0S2T", "type": "job.needs_review",
"created_at": "2026-11-03T09:14:05Z", "mode": "live",
"data": {"id": "job_01JAB2C9XY", "status": "needs_review",
"review_url": "https://api.accespub.app/portal/titulos/job_01JAB2C9XY/revision", …}}
Verify the signature before trusting the content (with the body exactly as received, without re-serialising it):
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300: # older than 5 minutes: reject
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
sig = hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
expected = base64.b64encode(sig).decode()
return any(hmac.compare_digest(expected, s.partition(",")[2])
for s in headers["webhook-signature"].split())
import crypto from "node:crypto";
function verify(secret, headers, rawBody) { // rawBody: Buffer
const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key)
.update(`${id}.${ts}.`).update(rawBody).digest("base64");
return headers["webhook-signature"].split(" ").some((s) => {
const sig = Buffer.from(s.split(",")[1] || "");
return sig.length === Buffer.byteLength(expected)
&& crypto.timingSafeEqual(sig, Buffer.from(expected));
});
}
- Reply with a
2xxwithin 10 seconds and process the notification afterwards. Any other response (a redirect too) is retried with increasing waits for about 25 hours (9 attempts). - The same notification may arrive twice (same
webhook-id): use it to drop duplicates. Order is not guaranteed either: when in doubt, read the current status withGET /jobs/{id}. - Test it with
POST /webhook-endpoints/{id}/test(sendswebhook.testright away and returns what your system replied). - Deliveries:
GET /webhook-endpoints/{id}/deliveries(30 days) and, to send one again,POST /webhook-endpoints/{id}/deliveries/{delivery_id}/retry. - Rotate the secret:
POST /webhook-endpoints/{id}/rotate-secret. For 24 hours each notification carries two signatures (new and previous); one match is enough. - If an address fails continuously for 5 days, it is disabled and the
administrators are told by email. Re-enable it with
PATCH /webhook-endpoints/{id}{"enabled": true}or from the portal. - For security, only public
httpsaddresses: nolocalhostor internal networks. To test locally, use a tunnel (for example, ngrok or Cloudflare Tunnel).
7. Errors, retries and limits
Errors follow RFC 9457 (application/problem+json). The title and detail
texts are in Spanish; rely on type and status in your code.
{
"type": "https://api.accespub.app/problems/file-too-large",
"title": "El EPUB supera el límite de tamaño",
"status": 413,
"detail": "atlas-de-dinosaurios.epub ocupa 64,3 MB; el límite es 50 MB."
}
| Code | Example type values |
What to do |
|---|---|---|
| 400, 422 | invalid-request, invalid-decision, not-an-epub, fixed-layout-not-supported, ai-blocked |
Fix the request or the file; write the text by hand |
| 401, 403 | unauthorized, forbidden |
Check the key or the role |
| 402 | insufficient-credits |
Top up credits |
| 409 | job-not-ready, job-not-finalized, pending-review, nothing-to-finalize, batch-limit-reached, storage-quota-exceeded |
Wait, review, create another batch or free up space |
| 410 | files-expired |
Files expired (30 days after finalisation): upload the EPUB again |
| 413 | file-too-large |
Reduce the EPUB or request a large upload |
| 429 | rate-limited |
Wait as indicated by Retry-After |
| 502, 503 | ai-error, ai-unavailable, server-storage-full |
Retry after Retry-After |
Idempotency. On operations that create or charge (create a batch, upload,
decide, finalise), send Idempotency-Key with a unique value. If the connection
fails and you repeat the request with the same key within 24 hours, you get the
original response without duplicating anything or being charged twice.
8. Credits
- One credit per title is used when it is finalised for the first time, if it has up to 150 images. Each additional block of 150 images (or part of one) uses one more credit: 151–300 images, 2 credits; 301–450, 3; and so on. All images in the content count, including those that already have correct alternative text.
- The number of images and the title's credits are in
billingas soon as the analysis ends (billing.images,billing.credits_required), before anything is reviewed. If it is not worth it, delete the title: nothing that is not finalised uses credits. - In the portal, a title that uses more than one credit shows a notice before the review starts: a batch manager confirms it or deletes the title.
- Reprocessing or finalising again a title that was already charged uses nothing more.
- Failed titles, or titles deleted before finalisation, use no credits.
- Without credits, finalising returns
402and nothing is charged. - Batches: all or nothing. If you ask to finalise 20 titles and 8 credits are
left, none is finalised; the error gives
credits_requiredandcredits_available. To finalise only some, passjob_ids. - A new version of an EPUB, even with the same ISBN, is a new job and uses another credit.
- Monthly usage:
GET /organization/usage.ap_test_keys never use credits.
9. Versions and changes
- The version is in the path (
/v1). Withinv1fields and operations are only added; they are never removed or change meaning. Ignore fields you do not know. - If something is going to be withdrawn, responses will carry the
Deprecation(RFC 9745) andSunset(RFC 8594) headers with the date, and you will be told by email at least 6 months in advance.
10. Resources
- Full reference, generated from the contract: /docs/referencia.
- OpenAPI 3.1 contract: openapi.yaml. Import it directly
into Postman, Insomnia or Bruno, or use it to generate clients
(
openapi-generator,openapi-typescript…). - Complete examples: quickstart.py (Python, with interactive review) and quickstart.mjs (Node.js 18+, no dependencies).
- Portal help for reviewers: /portal/ayuda.
- Test EPUBs: use your own with an
ap_test_key (no credits used). If you need samples with specific cases (images, tables, several languages, fixed layout), ask ab serveis. - Technical contact: info@abserveis.net.