Examples: correct a PDF end to end

Each example corrects one document from start to finish: upload it, state your authority to correct it, propose the correction, apply it, check the verification and download the result. Each reads three environment variables:

Variable What it is
CECO_URLThe address of this service, such as https://ceco.example
CECO_API_KEYYour key, ceco_live_…
CECO_PDFThe PDF to correct

The correction is the same in all three: the first "tije" on the first page becomes "tij". Change find and replacement for your own documents. Every example on this page is run against a real server by CECO's test suite, so what you read here works.

curl

The same as the Quickstart, for a shell script.

set -e
AUTH="Authorization: Bearer $CECO_API_KEY"
JSON="Content-Type: application/json"
field() { grep -o "\"$1\":\"[^\"]*\"" | head -n 1 | cut -d'"' -f4; }

DOC=$(curl -sf -H "$AUTH" -F "file=@$CECO_PDF;type=application/pdf" \
  "$CECO_URL/v1/documents" | field document_id)
ATT=$(curl -sf -H "$AUTH" -H "$JSON" -d "{\"document_id\":\"$DOC\"}" \
  "$CECO_URL/v1/attestations" | field attestation_id)

REQUEST="{\"page_index\":0,\"target\":{\"method\":\"text_layer\",\"find\":\"tije\",\"match_index\":0},\"replacement\":\"tij\",\"attestation_id\":\"$ATT\"}"
PROPOSAL=$(curl -sf -H "$AUTH" -H "$JSON" -d "$REQUEST" \
  "$CECO_URL/v1/documents/$DOC/edits:propose")
echo "$PROPOSAL" | grep -q '"refusal":null' || { echo "refused: $PROPOSAL"; exit 1; }
curl -sf -H "$AUTH" -H "$JSON" -o /dev/null \
  -d "{\"request\":$REQUEST,\"apply\":{\"proposal_id\":\"$(echo "$PROPOSAL" | field proposal_id)\",\"confirm_token\":\"$(echo "$PROPOSAL" | field confirm_token)\"}}" \
  "$CECO_URL/v1/documents/$DOC/edits"

PASSED=$(curl -sf -H "$AUTH" "$CECO_URL/v1/documents/$DOC/verification" \
  | grep -o '"passed":[a-z]*' | head -n 1)
[ "$PASSED" = '"passed":true' ] || { echo "not verified"; exit 1; }
curl -sf -H "$AUTH" -o corrected.pdf "$CECO_URL/v1/documents/$DOC/export/download"
echo "corrected.pdf written for $DOC"

Python (httpx)

import os

import httpx

headers = {"Authorization": f"Bearer {os.environ['CECO_API_KEY']}"}
with httpx.Client(base_url=os.environ["CECO_URL"], headers=headers, timeout=120) as ceco:
    with open(os.environ["CECO_PDF"], "rb") as pdf:
        uploaded = ceco.post(
            "/v1/documents", files={"file": ("invoice.pdf", pdf, "application/pdf")}
        ).raise_for_status().json()
    document_id = uploaded["document_id"]

    # Every correction carries a statement that you are authorised to make it.
    attestation = ceco.post(
        "/v1/attestations", json={"document_id": document_id}
    ).raise_for_status().json()

    request = {
        "page_index": 0,
        "target": {"method": "text_layer", "find": "tije", "match_index": 0},
        "replacement": "tij",
        "attestation_id": attestation["attestation_id"],
    }
    proposal = ceco.post(
        f"/v1/documents/{document_id}/edits:propose", json=request
    ).raise_for_status().json()
    if proposal["refusal"]:
        # `advice` says why, and what would change the answer.
        raise SystemExit(f"refused: {proposal['advice'] or proposal['refusal']}")

    ceco.post(f"/v1/documents/{document_id}/edits", json={
        "request": request,
        "apply": {
            "proposal_id": proposal["proposal_id"],
            "confirm_token": proposal["confirm_token"],
        },
    }).raise_for_status()

    verification = ceco.get(
        f"/v1/documents/{document_id}/verification"
    ).raise_for_status().json()
    if not verification["passed"]:
        raise SystemExit(f"not verified: {verification['failure_reason']}")

    corrected = ceco.get(f"/v1/documents/{document_id}/export/download").raise_for_status()
    with open("corrected.pdf", "wb") as out:
        out.write(corrected.content)
    print("corrected.pdf written for", document_id)

Waiting out a limit

429 too_many_requests and 503 busy both carry Retry-After, and a request answered with either did nothing, so it is safe to send again:

import os
import time

import httpx


def call(client: httpx.Client, method: str, url: str, **kwargs) -> httpx.Response:
    """Send a request, waiting out a rate limit or a busy engine."""
    for _ in range(5):
        response = client.request(method, url, **kwargs)
        if response.status_code not in (429, 503):
            return response
        time.sleep(int(response.headers.get("Retry-After", "1")))
    return response


headers = {"Authorization": f"Bearer {os.environ['CECO_API_KEY']}"}
with httpx.Client(base_url=os.environ["CECO_URL"], headers=headers, timeout=60) as ceco:
    usage = call(ceco, "GET", "/v1/usage").raise_for_status()
    print(usage.headers["X-RateLimit-Remaining"], "of", usage.headers["X-RateLimit-Limit"],
          "requests a minute left right now")

Node (fetch)

Node 18 or later, saved as an ES module and run with node correct.mjs.

import { readFileSync, writeFileSync } from "node:fs";

const base = process.env.CECO_URL;
const auth = { Authorization: `Bearer ${process.env.CECO_API_KEY}` };

async function ceco(method, path, body) {
  const init = { method, headers: { ...auth } };
  if (body instanceof FormData) {
    init.body = body;
  } else if (body !== undefined) {
    init.headers["Content-Type"] = "application/json";
    init.body = JSON.stringify(body);
  }
  const response = await fetch(base + path, init);
  if (!response.ok) {
    throw new Error(`${method} ${path}: ${response.status} ${await response.text()}`);
  }
  return response;
}

const form = new FormData();
const pdf = new Blob([readFileSync(process.env.CECO_PDF)], { type: "application/pdf" });
form.append("file", pdf, "invoice.pdf");
const { document_id } = await (await ceco("POST", "/v1/documents", form)).json();

// Every correction carries a statement that you are authorised to make it.
const { attestation_id } = await (
  await ceco("POST", "/v1/attestations", { document_id })
).json();

const request = {
  page_index: 0,
  target: { method: "text_layer", find: "tije", match_index: 0 },
  replacement: "tij",
  attestation_id,
};
const proposal = await (
  await ceco("POST", `/v1/documents/${document_id}/edits:propose`, request)
).json();
if (proposal.refusal) throw new Error(`refused: ${proposal.refusal}`);

await ceco("POST", `/v1/documents/${document_id}/edits`, {
  request,
  apply: { proposal_id: proposal.proposal_id, confirm_token: proposal.confirm_token },
});

const verification = await (
  await ceco("GET", `/v1/documents/${document_id}/verification`)
).json();
if (!verification.passed) throw new Error(`not verified: ${verification.failure_reason}`);

const corrected = await ceco("GET", `/v1/documents/${document_id}/export/download`);
writeFileSync("corrected.pdf", Buffer.from(await corrected.arrayBuffer()));
console.log(`corrected.pdf written for ${document_id}`);