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

  1. Ask ab serveis for an account. They will set up your organisation and invite your first administrator.
  2. Create the keys you need in the portal (Organisation → API keys) or with POST /api-keys ("mode": "test" for testing). A test key starts with ap_test_ and is shown only once: keep it in your secrets manager, never in your code.
  3. 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 with POST /invitations/accept.
  • API keys remain the way to integrate systems: an administration key acts as admin and the others as batch_manager.
  • If you build your own front end with personal sessions: POST /sessions returns a cookie and a csrf_token, which must be sent in the X-CSRF-Token header on requests that change something; use AccesPub-Mode: test for 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).

  1. 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}.
  2. 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).
  3. Upload the EPUBs and send metadata and commercial data from your catalogue (PATCH …/metadata, PATCH …/commercial).
  4. When a title reaches needs_review, show a “Review in AccesPub” button in your front end. Each title has portal_url (its page in the portal) and review_url (its review screen); each batch, its portal_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 opens review_url and the person signs in.
  5. When the review is done, the title is finalised on its own; your system gets the job.completed notification (webhooks, §6) and downloads the outputs. Without webhooks, poll GET /jobs/{id} until you see completed.

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 id in your system (POST /invitations returns it and GET /users lists 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_id stores 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.

  1. Get the pending items: GET /jobs/{id}/review-items?status=pending (paginated: up to 100 per page, follow next_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-decision with one error per decision in errors ("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-items shows in decided_by and decided_at who 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, reviewer is required and its email must be that of an active person in your organisation (or an external reviewer of that batch); otherwise 422 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) and all_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 with on_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-review marks the pending images, tables and inline languages as decorative, layout or kept. With "confirm_decorative": true and reviewer, 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}/metadata shows each field with its origin (epub or override) 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) and title_placeholder (the EPUB had no title).
  • Notices do not block, as in the dashboard, but they are worth resolving.
  • null removes 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 ES or FR BE CH, or ONIX regions such as WORLD) with its currency, price, price type, tax, availability, supplier and on-sale date. Several currencies in one territory: repeat the territory.
  • GET /jobs/{id}/commercial returns the effective values, where each one comes from and the checks (issues); with any error, POST /onix-full returns 422.
  • apply_to_batch in 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) and GET /batches/{id}/files/onix_full.
  • The cost is set by the organisation's contract (billing in 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 2xx within 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 with GET /jobs/{id}.
  • Test it with POST /webhook-endpoints/{id}/test (sends webhook.test right 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 https addresses: no localhost or 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 billing as 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 402 and 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_required and credits_available. To finalise only some, pass job_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). Within v1 fields 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) and Sunset (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.

Back to top