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.
- Accodi con
POST /v1/operations/<nome>→ ricevi202con unid. - Chiedi lo stato con
GET /v1/jobs/<id>finché non ècompletato. - 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_coda → in_lavorazione → completato,
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.
| Operazione | File | Che cosa fa |
|---|---|---|
inspect | uno | Legge il documento senza modificarlo: pagine, dimensioni, moduli, firme. Con signatures: true verifica le firme digitali. |
merge | molti | Unisce più PDF in uno, nell'ordine in cui li mandi. Nessuna opzione. |
split | uno | Divide in più documenti. ranges è una lista. compact: false salta la ricompattazione: più veloce, pezzi molto più pesanti. |
organize | molti | Ruota, riordina, elimina, inserisce: una lista di passi in steps. |
page-tools | uno | Filigrana, intestazioni, numerazione Bates, ritaglio. Serve almeno uno fra watermark, header_footer, bates, crop. |
forms/read | uno | Legge i campi di un modulo AcroForm e i loro valori. |
forms/fill | uno | 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. |
protect | uno | Password e permessi. user_password serve per aprire, owner_password per cambiare i permessi. Oltre a print e modify ci sono extract e accessibility. |
unprotect | uno | Toglie la protezione, se hai la password. |
compress | uno | Riduce il peso. target_bytes è un obiettivo, non una garanzia. |
redact | uno | Oscura porzioni rimuovendo il contenuto, non coprendolo con un rettangolo nero. Toglie anche i metadati (metadata) e, se lo chiedi, gli allegati (attachments). |
convert/text | uno | Estrae il testo. Su una scansione senza testo lo dice invece di restituire un file vuoto. |
convert/images | uno | Pagine in immagini. Operazione pesante: pesa di più sulla concorrenza. |
| Modificare il contenuto | ||
edit/map | uno | 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/text | uno | 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/objects | uno | Gli oggetti di una pagina: list li elenca con indice, tipo e riquadro; ops applica move, resize, delete, add_image, replace_image. Operazione pesante. |
annotate | uno | 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/find | uno | Le stesse annotazioni senza avere le coordinate: query è il testo da cercare. Con dry_run vedi dove finirebbero prima di scriverle. Operazione pesante. |
sign/stamp | uno | Firma visibile, sigla, data, timbro: stamps è una lista. Non è una firma digitale. Un documento già firmato viene rifiutato, perché riscriverlo farebbe sparire la firma. |
links | uno | 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/manage | uno | 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
MISSING_KEY— mancaAuthorization.MALFORMED_KEY— la chiave non ha la forma attesa.INVALID_KEY— la chiave non esiste o il segreto è sbagliato.SUSPENDED_KEY— chiave o account sospesi. La sospensione è reversibile: scrivici.MISSING_FIELD/INVALID_FIELD— problema nel corpo; il campo è incampo.NOT_FOUND— operazione, lavoro o risultato inesistente.UNSUPPORTED_MEDIA— il documento o un'immagine allegata non sono trattabili così: è quello che ricevi dasign/stampe dalinkssu un PDF già firmato, e dasign/stampsu un'immagine che non è né PNG né JPEG.IP_NOT_ALLOWED— la chiave è valida ma non da questo indirizzo. Se il tuo server è cambiato, aggiorna gli indirizzi ammessi.RATE_LIMITED— quota esaurita, conretry_after.SERVER_BUSY— la macchina è piena. Riprova più tardi.BODY_TOO_LARGE— il corpo della richiesta supera il tetto.FILE_TOO_LARGE— un singolo documento supera il tetto.
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:
- il file vive in uno spool fuori dai backup e viene cancellato entro 24 ore;
- i nomi dei file nello spool sono opachi: non contengono il tuo nome file;
- non conserviamo il contenuto dei documenti né lo indicizziamo;
- nel registro tecnico restano l'operazione, i byte e la durata — non il documento.
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.