Guida all'API

API di PDFEditabile

Ventuno operazioni sui PDF, chiamabili da qualunque linguaggio. Il servizio è gratuito e le chiavi si chiedono dall'area riservata: non vengono date a tutti, perché la macchina è una sola.

I file non restano. Un documento caricato vive nello spool per il tempo della lavorazione e il risultato è scaricabile per 24 ore; dopo viene cancellato. Non c'è archivio, non c'è ricerca, non c'è modo di recuperare un file di ieri — né per te né per noi.

Provare senza chiave

Esiste una chiave dimostrativa pubblica, con limiti molto bassi, che non richiede account. Serve a capire in cinque minuti se l'API fa quello che ti serve:

curl -X POST https://pdfeditabile.it/v1/operations/merge \
  -H "Authorization: Bearer pe_demo_pubblica" \
  -H "Content-Type: application/json" \
  -d '{"files": ["JVBERi0xLjQK...", "JVBERi0xLjQK..."]}'

La corsia dimostrativa non accetta l'intestazione di idempotenza e ha un tetto di dimensione molto più basso. Per qualunque cosa oltre la prova, serve una chiave.

La chiave

Si chiede dall'area riservata dopo la registrazione, compilando un modulo: chi la chiede spiega a cosa gli serve, e noi decidiamo. Le richieste ricevono risposta entro cinque giorni lavorativi.

Una chiave ha questa forma, e si manda come bearer token:

pe_<ambiente>_<kid 10 caratteri>_<segreto 43><controllo 6>

Authorization: Bearer pe_live_a1B2c3D4e5_...

Il segreto lo vedi una volta sola. Nel nostro database c'è solo la sua impronta HMAC, calcolata con un pepe che sta fuori dal database: non è recuperabile da nessuno, noi compresi. Se la perdi, ne emetti un'altra dalla tua area e quella vecchia resta valida per una finestra di grazia, così l'integrazione non si ferma nel momento del cambio.

Legare la chiave a un indirizzo

Una chiave può essere limitata agli indirizzi da cui è utilizzabile — il server che chiama l'API, non il tuo portatile. Da qualunque altro indirizzo la risposta è 403 con IP_NOT_ALLOWED.

È difesa in profondità, non un sostituto del segreto. Una chiave finisce in un file di configurazione, in una variabile d'ambiente, nella cronologia di una shell: legarla a un indirizzo fa sì che chi la trova non possa usarla dalla propria macchina. Chi controlla quella rete la aggira lo stesso — quindi la chiave va comunque custodita.

Ogni rifiuto per indirizzo finisce nel nostro log. Se la tua chiave è legata e chiami sempre da dove devi, quelle righe non compaiono mai: quando compaiono, è il segnale più forte che il segreto sia uscito. Chiedila tu, la restrizione, se il tuo server ha un indirizzo stabile.

Come funziona una chiamata

È asincrona, e non per scelta stilistica: unire quaranta PDF o oscurare un documento scansionato richiede secondi, e una connessione HTTP tenuta aperta per quel tempo cade nel momento peggiore — a lavoro fatto e prima della risposta.

  1. Accodi con POST /v1/operations/<nome> → ricevi 202 con un id.
  2. Chiedi lo stato con GET /v1/jobs/<id> finché non è completato.
  3. Scarichi da GET /v1/jobs/<id>/results/<idRisultato>.

Il corpo della richiesta

{
  "files":   [ "<base64>", { "content": "<base64>", "nome": "contratto.pdf" } ],
  "options": { ... }
}

files è obbligatorio: un elenco in cui ogni voce è una stringa base64 oppure un oggetto con content (o contenuto). Le opzioni stanno in options e cambiano da un'operazione all'altra: sono elencate qui sotto.

Scaricare il risultato

Quando il lavoro è completato, la risposta porta un elenco results. Ogni voce ha un id, il nome, il tipo, i byte e lo sha256 del contenuto: puoi verificare l'integrità di quello che hai scaricato senza fidarti del trasporto.

"results": [
  { "id": "o3wQatyhcOa-EanbeBx1zw", "nome": "merged.pdf",
    "tipo": "application/pdf", "byte": 1254, "sha256": "ad0cf0b1…" }
]

GET /v1/jobs/<id>/results/o3wQatyhcOa-EanbeBx1zw
→ 200 application/pdf
  content-disposition: attachment; filename="merged.pdf"
  cache-control: private, no-store

Gli stati di un lavoro

in_codain_lavorazionecompletato, oppure fallito, annullato, scaduto, purgato. Sono gli stessi valori sia nella risposta alla creazione sia in lettura.

Ripetere senza raddoppiare

Con una chiave vera l'intestazione Idempotency-Key è obbligatoria. Non è burocrazia: senza, un ritentativo dopo un timeout di rete rifà il lavoro e ricarica i file, e tu paghi due volte in quota quello che hai chiesto una volta sola. Stessa chiave con gli stessi file torna lo stesso lavoro.

Le operazioni

Il nome va nel percorso: POST /v1/operations/<nome>. Le opzioni elencate qui sono quelle che il servizio legge davvero.

I nomi delle opzioni non bastano: conta la forma. La prima versione di questa pagina elencava «ranges» e chi la seguiva mandava "ranges": "1" ricevendo un errore. Qui sotto ci sono gli esempi, provati contro il servizio.

OperazioneFileChe cosa fa
inspectuno Legge il documento senza modificarlo: pagine, dimensioni, moduli, firme. Con signatures: true verifica le firme digitali.
mergemolti Unisce più PDF in uno, nell'ordine in cui li mandi. Nessuna opzione.
splituno Divide in più documenti. ranges è una lista. compact: false salta la ricompattazione: più veloce, pezzi molto più pesanti.
organizemolti Ruota, riordina, elimina, inserisce: una lista di passi in steps.
page-toolsuno Filigrana, intestazioni, numerazione Bates, ritaglio. Serve almeno uno fra watermark, header_footer, bates, crop.
forms/readuno Legge i campi di un modulo AcroForm e i loro valori.
forms/filluno Compila i campi. Con flatten: true il modulo diventa non più modificabile. Se il PDF non ha campi, risponde che non c'è niente da compilare.
protectuno Password e permessi. user_password serve per aprire, owner_password per cambiare i permessi. Oltre a print e modify ci sono extract e accessibility.
unprotectuno Toglie la protezione, se hai la password.
compressuno Riduce il peso. target_bytes è un obiettivo, non una garanzia.
redactuno Oscura porzioni rimuovendo il contenuto, non coprendolo con un rettangolo nero. Toglie anche i metadati (metadata) e, se lo chiedi, gli allegati (attachments).
convert/textuno Estrae il testo. Su una scansione senza testo lo dice invece di restituire un file vuoto.
convert/imagesuno Pagine in immagini. Operazione pesante: pesa di più sulla concorrenza.
Modificare il contenuto
edit/mapuno La mappa modificabile: per ogni pagina i blocchi di testo con indice, contenuto, corpo, font, colore e coordinate — e se sono ri-impaginabili, e perché no. Leggila prima di chiamare edit/text: senza, non sai che cosa chiedere. Con runs anche gli intervalli di stile, con objects gli oggetti grezzi, con pages ci si limita a un intervallo. Operazione pesante.
edit/textuno Modifica il testo: edits è una lista di operazioni — set_text (riscrive un blocco ri-impaginandolo), replace (corregge un termine ovunque), set_style (corpo, carattere, colore), set_line (riscrive una riga senza spostare nient'altro), add_text. O si applicano tutte o non esce nessun file: on_error: "skip" per accettare un risultato parziale. Operazione pesante.
edit/objectsuno Gli oggetti di una pagina: list li elenca con indice, tipo e riquadro; ops applica move, resize, delete, add_image, replace_image. Operazione pesante.
annotateuno Annotazioni: add aggiunge evidenziazioni, sottolineature, barrature, note, testo libero, matita, linee, frecce, rettangoli e ovali; remove le toglie; author firma quelle nuove; import_xfdf ed export_xfdf spostano i commenti da un documento all'altro.
annotate/finduno Le stesse annotazioni senza avere le coordinate: query è il testo da cercare. Con dry_run vedi dove finirebbero prima di scriverle. Operazione pesante.
sign/stampuno Firma visibile, sigla, data, timbro: stamps è una lista. Non è una firma digitale. Un documento già firmato viene rifiutato, perché riscriverlo farebbe sparire la firma.
linksuno I collegamenti: senza opzioni li elenca e basta; add, update e remove li aggiungono, li ripuntano e li tolgono. page limita l'elenco a una pagina o a un intervallo.
fields/manageuno Costruisce un modulo: create, update, rename, delete e tab_order. Quello che crei qui lo leggi con forms/read e lo compili con forms/fill.

Esempi di options, provati

inspect        {"signatures": true}
merge          {}          nessuna opzione: l'ordine è quello in cui mandi i file
split          {"ranges": ["1-3", "5", "7-"], "compact": false}
               ranges:  una LISTA, non una stringa
               compact: true (default) ricompatta ogni pezzo — un pezzo passa da
                        ~106 KB a ~1,4 KB, al costo di 20-80 ms ciascuno
organize       {"steps": [{"op": "rotate", "pages": "1", "degrees": 90}]}
               rotate       pages + degrees
               delete       pages
               duplicate    pages
               reorder      order   ⚠ una LISTA: [3,1,2]
               insert_blank at      (+ size opzionale)  ⚠ «at», non «pages»
               insert_pdf   at + source
page-tools     {"watermark": {"text": "BOZZA"}}
               {"header_footer": {"pageNumber": true}}   (anche «page_number», «numero»)
               blocchi: watermark | header_footer | bates | crop — almeno uno
forms/read     {}          nessuna opzione: elenca i campi del modulo e i valori
forms/fill     {"fields": {"nome": "Mario Rossi"}, "flatten": true}
protect        {"user_password": "…", "print": "none", "modify": "none"}
               print:  none | low | high
               modify: none | comments | forms | assembly | all
               extract:       false  vieta di copiare testo e immagini   (default true)
               accessibility: false  vieta l'estrazione da parte dei lettori
                                     per non vedenti                     (default true)
unprotect      {"password": "…"}
compress       {"target_bytes": 500000}
redact         {"redactions": [{"page": 1,
                  "rects": [{"x": 72, "y": 700, "width": 220, "height": 30}]}],
                "metadata": true, "attachments": false}
               metadata:    true (default) toglie autore, software, percorso d'origine
               attachments: true toglie anche gli allegati  (default false:
                            è distruttivo, e chi non lo chiede non se lo aspetta)
convert/text   {"pages": "1-3"}
convert/images {"pages": "1", "dpi": 150}
               annotations: true (default) rende anche evidenziazioni, note e
                            commenti, come fa qualunque lettore PDF. Metti
                            false per l'immagine del solo contenuto.
               dpi: da 36 a 600, default 150

── modificare il contenuto ──────────────────────────────────────────────

edit/map       {"pages": "1", "runs": true}
               pages:   "1-3,5", [1,2] oppure "all" — se manca, tutte
               runs:    true aggiunge gli intervalli di stile dentro ogni blocco
               objects: true aggiunge gli oggetti grezzi della pagina
               è una LETTURA: non produce nessun file, come inspect

edit/text      {"edits": [{"op": "add_text", "page": 1, "x": 72, "y": 120,
                           "value": "Aggiunto via API", "size": 9}]}
               op: set_text | replace | set_style | set_line | add_text
               dove lavorare (tutte tranne add_text):
                 page        restringe a una pagina
                 text_is     il testo ESATTO del blocco      ⚠ text_is e text_has
                 text_has    un pezzo del testo del blocco     sono alternativi
                 paragraph   l'indice del blocco preso da edit/map (vuole «page»)
                 occurrence  1 = il primo, se il criterio ne prende più d'uno
               set_text   value + cascade (default true: ri-impagina anche i
                          blocchi sotto) + margin
               replace    find + with + case_sensitive + all
               set_style  almeno una fra font, size, color; con start ed end
                          (offset nel testo del blocco, «end» escluso) si stila
                          solo un pezzo
               set_line   page + text_is/text_has + value
               add_text   page + x + y + value + size + font + color + line_height
               ⚠ conviene indicare i blocchi per CONTENUTO — text_is / text_has —
                 perché ogni ri-impaginazione RINUMERA gli indici. Un «paragraph»
                 preso da edit/map vale solo finché quella pagina non è stata
                 toccata: dopo viene rifiutato, non «corretto» in silenzio.
               on_error: "fail" (default, o tutto o niente) | "skip"
               ⚠ massimo 200 modifiche per chiamata
               allow_signed: true  procede su un documento già firmato —
                             ⚠ la firma resta ma risulta COMPROMESSA.
                             Di default è rifiutato.

edit/objects   {"list": true}
               {"ops": [{"op": "move", "page": 1, "object": {"index": 3},
                         "dx": -100, "dy": 0}]}
               list: true (tutte le pagine) | "1-3,5" | [1,2] — con «list» sola
                     è una lettura e non esce nessun file
               op: move | resize | delete | add_image | replace_image
                 move           object + dx + dy
                 resize         object + scale_x/scale_y OPPURE width/height
                                (in punti), + anchor
                 delete         object
                 add_image      image + x + y (+ width, height)
                 replace_image  object + image
               object:  {"index": n} | {"at": {"x":…, "y":…}} | {"type": "image",
                        "nth": 0}   ⚠ un solo modo per volta; «nth» parte da 0
               anchor:  bottom_left (default) | bottom_right | top_left |
                        top_right | center, oppure un punto {"x":…, "y":…}
               image:   {"data": "<base64 PNG o JPEG>", "format": "png"}
                        («format» è facoltativo: i byte comandano)
               ⚠ NON si applicano nell'ordine del JSON: prima move e resize,
                 poi le eliminazioni dall'indice più ALTO al più basso, infine
                 gli inserimenti. Gli indici valgono sull'elenco di PARTENZA.
                 Il campo «applied» della risposta riporta l'ordine vero.
               allow_signed: true  procede su un documento già firmato —
                             ⚠ la firma resta ma risulta COMPROMESSA.
                             Di default è rifiutato (solo con «ops»: «list»
                             legge e basta, quindi non si rifiuta).
               tetti: 100 modifiche e 20 immagini per chiamata

annotate       {"add": [{"type": "highlight", "page": 1,
                         "rect": {"x": 72, "y": 700, "width": 220, "height": 14}}]}
               type: highlight | underline | strikeout | note | freetext |
                     ink | line | arrow | rect | oval
               ⚠ si chiamano «rect» e «oval», non «square» e «circle»
               ogni voce di add accetta, oltre a type e page:
                 text (o contents)  il commento           author  chi la firma
                 created            data ISO 8601         state   Accepted |
                 color  «#ffd400»   opacity  da 0 a 1             Rejected |
                                                                  Cancelled |
                                                                  Completed
               e la geometria, che cambia col tipo:
                 highlight/underline/strikeout  rect  OPPURE  quads: una lista di
                        {x1,y1,x2,y2,x3,y3,x4,y4} (alto-sx, alto-dx, basso-sx,
                        basso-dx). Con «rect» l'ordine lo mettiamo noi
                 note                           rect — bastano x e y: l'icona è
                                                di 18 punti fissi
                 freetext                       rect + text (obbligatorio) + font_size
                 rect / oval                    rect + fill (riempimento) + stroke_width
                 ink                            paths: [[{"x":…,"y":…}, …], …]
                                                + stroke_width
                 line / arrow                   x1, y1, x2, y2 + stroke_width
               remove: "all"  oppure  {"indexes": [0, 2], "pages": "1-3",
                       "types": ["highlight"]}  — i criteri si sommano, e gli
                       indici sono quelli dell'elenco «annotations» letto ORA
               import_xfdf: "<xfdf …>"   export_xfdf: true
                allow_signed: true  procede su un documento già firmato —
                              ⚠ E LA FIRMA SPARISCE, non risulta «non valida».
                              Di default è rifiutato.
                ⚠ massimo 2000 annotazioni per chiamata
               ⚠ l'ordine dentro una chiamata è fisso: remove → import_xfdf → add,
                 così {"remove":"all","import_xfdf":…} vuol dire «sostituisci»

annotate/find  {"query": "Trasparenzia", "type": "highlight"}
               per chi NON ha le coordinate: cerca il testo e annota dov'è
               type: highlight | underline | strikeout | rect
                     ⚠ solo questi quattro: gli altri non hanno senso su un
                       risultato di ricerca e vogliono le coordinate di annotate
               color, opacity, text, author  come in annotate
               match_case, whole_word:  restringono la ricerca
               max_matches:             tetto alle occorrenze (default 500).
                                        Sopra il tetto la chiamata è RIFIUTATA,
                                        non troncata in silenzio
               dry_run: true            dice DOVE finirebbero senza scriverle
                ⚠ max_matches non può superare 5000
               pages:                   limita la ricerca

sign/stamp     {"stamps": [{"page": 1, "x": 72, "y": 100, "width": 200,
                            "height": 40, "text": "Mario Rossi",
                            "kind": "signature"}]}
               kind: signature | initials | date | text | image
                     «date» scrive la data di oggi se non gli dai un «text»
               dove: page + x + y + width + height, OPPURE «field» — il nome di
                     un campo firma del documento, di cui prendiamo il riquadro
                     (⚠ i due modi non si mescolano)
               image:     PNG o JPEG in base64 (con o senza prefisso «data:»)
               text:      il testo da scrivere
               font:      Courier | CourierBold | CourierOblique |
                          CourierBoldOblique | Helvetica | HelveticaBold |
                          HelveticaOblique | HelveticaBoldOblique | TimesRoman |
                          TimesRomanBold | TimesRomanItalic | TimesRomanBoldItalic
                          ⚠ non c'è nessun font manoscritto, e i font standard
                            scrivono solo WinAnsi: niente cirillico né ideogrammi
               font_size: se manca, il corpo più grande che sta nel riquadro
               color:     «#rrggbb» o [r,g,b] 0-255      (default «#12305e»)
               opacity:   da 0 a 1                       (default 1)
               align:     left | center | right   (default: center per «initials»,
                          left per gli altri)
               fit:       contain (default, conserva le proporzioni) | stretch
               allow_signed: true  procede su un documento già firmato —
                ⚠ massimo 50 timbri per chiamata, di cui 10 con immagine
                             ⚠ E LA FIRMA SPARISCE. Di default è rifiutato
                             con UNSUPPORTED_MEDIA.
               ⚠ è GRAFICA: un segno dentro un campo firma NON riempie quel campo

links          {"add": [{"page": 1, "x": 72, "y": 700, "width": 200,
                         "height": 24, "url": "https://pdfeditabile.it/"}]}
               senza add/update/remove ELENCA e basta, e non produce file
               add:    page + x + y + width + height + (url oppure target_page)
               update: le stesse chiavi + index (di quel link nella sua pagina),
                       e almeno una cosa da cambiare
               remove: una LISTA di {"page": 1, "index": 0}
               url:         solo http:, https:, mailto: e tel:
               target_page: la pagina di destinazione, per un link interno
               border:      true disegna il riquadro (default false: invisibile)
               page:        limita l'ELENCO — un numero, «1-3,5» o [1,2]
               ⚠ nell'«url» i caratteri sopra U+00FF vanno percent-codificati
                 (encodeURI): oltre quella soglia il carattere non arriverebbe
                 intatto nel PDF, e la richiesta viene rifiutata
               ⚠ massimo 5000 collegamenti per chiamata (add+update+remove)
               ⚠ modificare i link di un documento FIRMATO è rifiutato
                 (UNSUPPORTED_MEDIA); elencarli no, perché non lo riscrive

fields/manage  {"create": [{"name": "cognome", "type": "text", "page": 1,
                            "rect": {"x": 60, "y": 60, "width": 220, "height": 22}}]}
               type: text | checkbox | radio | dropdown | optionlist | signature
                     («testo multiriga» non è un tipo: è text + multiline)
               create: name + type + (page + rect) OPPURE widgets, e in più
                 value       il valore iniziale
                 options     le scelte — obbligatorie per radio, dropdown, optionlist
                 required    obbligatorio da compilare
                 readonly    non modificabile
                 tooltip     il suggerimento che appare al passaggio del mouse
                 multiline   solo text: casella a più righe
                 max_length  solo text: quanti caratteri al massimo
                 comb        solo text: caselline separate (vuole max_length)
                 widgets     una LISTA di {"page", "rect"}: lo stesso campo su più
                             riquadri. ⚠ per «radio» ce ne vuole UNO PER OPZIONE
               update: le stesse proprietà (name + value, options, required,
                       readonly, tooltip, multiline, max_length, comb).
                       ⚠ non sposta e non ritipizza: per quello si elimina e si ricrea
               rename: {"vecchio": "nuovo"}     delete: ["nome", …]
               tab_order: true raddrizza l'ordine di tabulazione
               ⚠ nel rect si può scrivere "origin": "top-left" se stai
                 ragionando dall'alto: la conversione la facciamo noi
                 (l'altro valore è "bottom-left", che è il default del PDF)
               ⚠ massimo 1000 campi per chiamata (create+update+rename+delete)
               ⚠ ordine fisso: delete → create → update → rename → tab_order

I rettangoli di redact vogliono width e height per esteso — non w e h — e le coordinate sono in punti PDF con l'origine in basso a sinistra, come vuole il formato: y cresce verso l'alto, non verso il basso.

Un'opzione che non esiste viene rifiutata, non ignorata. Se scrivi wattermark al posto di watermark, o w al posto di width, ricevi un 400 che elenca i nomi ammessi in quel punto e, quando è chiaro, ti dice quale intendevi. Prima ricevevi 200 e un documento non toccato: è il caso peggiore, perché il problema sembra tuo.

Un nome che non è in questo elenco riceve 404 prima ancora che il corpo venga letto. E anche la forma delle opzioni viene controllata alla chiamata: ranges come stringa, un verbo di organize che non esiste, print come booleano o un blocco di page-tools mancante ricevono 400 subito — non un 202 seguito da un lavoro fallito.

Quello che invece si scopre solo dopo è tutto ciò che richiede di aprire il documento: un intervallo di pagine oltre l'ultima, un modulo senza campi, una password sbagliata. Quelli restano errori del lavoro, leggibili da GET /v1/jobs/<id>, perché per accorgersene bisogna leggere il PDF — e a quel punto il lavoro è già cominciato.

I limiti

I limiti in vigore sono leggibili dal servizio stesso, senza autenticazione, e sono la fonte da credere se questa pagina dovesse restare indietro:

GET https://pdfeditabile.it/v1/capabilities

Ogni risposta autenticata porta anche lo stato della tua quota nelle intestazioni:

ratelimit-limit: 5
ratelimit-remaining: 3
ratelimit-reset: 24
ratelimit-policy: 5;w=60;burst=5, 100;w=86400

Quando la quota finisce arriva 429 con retry-after. Se invece è il servizio a essere pieno arriva 503: sono due cose diverse — il primo dice «hai esagerato tu», il secondo «siamo pieni noi» — e vanno trattate in modo diverso da chi ritenta.

Gli errori

Ogni errore è un RFC 9457 application/problem+json, con un code stabile su cui scrivere il proprio codice — il detail è per le persone e può cambiare.

{
  "type":     "https://pdfeditabile.it/errori/invalid-field",
  "title":    "Campo non valido",
  "status":   400,
  "code":     "INVALID_FIELD",
  "detail":   "Il documento 1 non ha un contenuto in base64.",
  "instance": "req_8D50aJRDfL7j",
  "campo":    "files[0]"
}

instance è l'identificatore della richiesta: è nell'intestazione x-request-id di ogni risposta, ed è la cosa da citare se ci scrivi.

I codici che incontrerai più spesso

Privacy, detto per esteso

L'editor nel browser lavora interamente sul dispositivo: quei file non arrivano mai qui. L'API è diversa e va detto chiaramente — per elaborare un documento dobbiamo riceverlo. Quello che facciamo:

Se tratti dati particolari, dichiaralo nella richiesta di chiave: cambia quello che possiamo accettare. Per il resto vale la privacy policy.

Se qualcosa non va

Scrivi a support@gtechgroup.it citando l'x-request-id della richiesta che non è andata: con quello troviamo la riga esatta nel registro del servizio.