Sign a PDF
curl -H "X-Api-Key: $KEY" -F files=@contract.pdf -F signature=certificate \ -F reason=Approved -F location=Nairobi -F level=b-lt \ http://localhost:9027/v1/pdf/sign -o signed.pdfimport { openAsBlob } from 'node:fs';import { writeFile } from 'node:fs/promises';
const form = new FormData();form.append('files', await openAsBlob('contract.pdf'), 'contract.pdf');form.append('signature', 'certificate');form.append('reason', 'Approved');form.append('location', 'Nairobi');form.append('level', 'b-lt');
const response = await fetch('http://localhost:9027/v1/pdf/sign', { method: 'POST', headers: { 'X-Api-Key': process.env.KEY }, body: form,});if (!response.ok) throw new Error(await response.text());await writeFile('signed.pdf', Buffer.from(await response.arrayBuffer()));import osfrom pathlib import Path
import requests
response = requests.post( "http://localhost:9027/v1/pdf/sign", headers={"X-Api-Key": os.environ["KEY"]}, files=[("files", open("contract.pdf", "rb"))], data={ "signature": "certificate", "reason": "Approved", "location": "Nairobi", "level": "b-lt", },)response.raise_for_status()Path("signed.pdf").write_bytes(response.content)package main
import ( "bytes" "io" "log" "mime/multipart" "net/http" "os" "path/filepath")
func main() { var body bytes.Buffer form := multipart.NewWriter(&body) attach(form, "files", "contract.pdf") form.WriteField("signature", "certificate") form.WriteField("reason", "Approved") form.WriteField("location", "Nairobi") form.WriteField("level", "b-lt") form.Close()
request, err := http.NewRequest("POST", "http://localhost:9027/v1/pdf/sign", &body) if err != nil { log.Fatal(err) } request.Header.Set("X-Api-Key", os.Getenv("KEY")) request.Header.Set("Content-Type", form.FormDataContentType())
response, err := http.DefaultClient.Do(request) if err != nil { log.Fatal(err) } defer response.Body.Close() if response.StatusCode >= 300 { message, _ := io.ReadAll(response.Body) log.Fatalf("%s: %s", response.Status, message) }
out, err := os.Create("signed.pdf") if err != nil { log.Fatal(err) } defer out.Close() if _, err := io.Copy(out, response.Body); err != nil { log.Fatal(err) }}
// attach adds the file at path to the form as the named field.func attach(form *multipart.Writer, field, path string) { file, err := os.Open(path) if err != nil { log.Fatal(err) } defer file.Close() part, err := form.CreateFormFile(field, filepath.Base(path)) if err != nil { log.Fatal(err) } if _, err := io.Copy(part, file); err != nil { log.Fatal(err) }}curl -H "X-Api-Key: $KEY" -F files=@signed.pdf -F signature=renew \ http://localhost:9027/v1/pdf/sign -o renewed.pdfimport { openAsBlob } from 'node:fs';import { writeFile } from 'node:fs/promises';
const form = new FormData();form.append('files', await openAsBlob('signed.pdf'), 'signed.pdf');form.append('signature', 'renew');
const response = await fetch('http://localhost:9027/v1/pdf/sign', { method: 'POST', headers: { 'X-Api-Key': process.env.KEY }, body: form,});if (!response.ok) throw new Error(await response.text());await writeFile('renewed.pdf', Buffer.from(await response.arrayBuffer()));import osfrom pathlib import Path
import requests
response = requests.post( "http://localhost:9027/v1/pdf/sign", headers={"X-Api-Key": os.environ["KEY"]}, files=[("files", open("signed.pdf", "rb"))], data={"signature": "renew"},)response.raise_for_status()Path("renewed.pdf").write_bytes(response.content)package main
import ( "bytes" "io" "log" "mime/multipart" "net/http" "os" "path/filepath")
func main() { var body bytes.Buffer form := multipart.NewWriter(&body) attach(form, "files", "signed.pdf") form.WriteField("signature", "renew") form.Close()
request, err := http.NewRequest("POST", "http://localhost:9027/v1/pdf/sign", &body) if err != nil { log.Fatal(err) } request.Header.Set("X-Api-Key", os.Getenv("KEY")) request.Header.Set("Content-Type", form.FormDataContentType())
response, err := http.DefaultClient.Do(request) if err != nil { log.Fatal(err) } defer response.Body.Close() if response.StatusCode >= 300 { message, _ := io.ReadAll(response.Body) log.Fatalf("%s: %s", response.Status, message) }
out, err := os.Create("renewed.pdf") if err != nil { log.Fatal(err) } defer out.Close() if _, err := io.Copy(out, response.Body); err != nil { log.Fatal(err) }}
// attach adds the file at path to the form as the named field.func attach(form *multipart.Writer, field, path string) { file, err := os.Open(path) if err != nil { log.Fatal(err) } defer file.Close() part, err := form.CreateFormFile(field, filepath.Base(path)) if err != nil { log.Fatal(err) } if _, err := io.Copy(part, file); err != nil { log.Fatal(err) }}Signs a PDF. With signature=certificate, a cryptographic signature with
the key in RUSTICDOC_SIGNING_KEY and RUSTICDOC_SIGNING_CERT, or the
named key signingKey chooses: an incremental update with a PAdES
/ETSI.CAdES.detached signature at the level asked for (B-B to B-LTA),
approving or, with certify, certifying. It is invisible, shown with the
one image sent, or, with a position and no image, shown as text naming
the signer, the date and the reason. Without a configured key that answers
503; a timestamp authority or revocation source that fails answers 502. By
default (signature=image) the images are drawn at exact coordinates,
which is not a cryptographic signature. signature=renew signs nothing:
it renews the archive timestamp of a file already signed at B-LTA, which
must end with a document timestamp from an authority this service trusts
and be unchanged since; anything else answers 400.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
With signature=certificate: certify rather than approve, with this
DocMDP permission: 1 no changes, 2 form filling and signing, 3
also annotations. Only the document’s first signature can certify.
With signature=certificate: how to reach the signer.
The PDF first, then one PNG or JPEG per placement. With
signature=certificate, at most one image: the visible appearance.
With signature=renew, the PDF alone.
With signature=certificate: the PAdES level, b-b, b-t (with an
RFC 3161 timestamp), b-lt (and validation data in a DSS) or b-lta
(and a document timestamp). Default the service’s
RUSTICDOC_SIGNING_LEVEL, b-b unless set.
Example
b-ltWith signature=certificate: where it was signed.
1-based page of each image; repeat the field for several.
With signature=certificate: why the document was signed.
image (default) draws the images and is not a cryptographic
signature. certificate signs with the service’s key (PAdES,
/ETSI.CAdES.detached), invisibly unless an image or a position is
sent. renew takes a file signed at b-lta and adds validation data
for its last document timestamp and a new document timestamp, which
keeps the signature verifiable after the old timestamp’s certificate
expires; it takes no other field than outputFilename.
Example
certificateWith signature=certificate: the signer’s name as shown. Validators
identify the signer by the certificate, not by this.
With signature=certificate: a named key from RUSTICDOC_SIGNING_KEYS
instead of the default one. A caller not on its allow list gets 403.
Width in points. Default the image’s natural size, or 200 for a text appearance.
Points from the page’s left edge to the image’s left edge.
Points from the page’s bottom edge to the image’s bottom edge.
Responses
Section titled “Responses”The signed PDF
The file, as bytes.
Accepted; the result goes to the webhook
The request is wrong
The JSON body of every error response.
object
A stable, machine-readable kind.
What went wrong, for a person.
Example
{ "error": "invalid_request", "message": "unknown step 'teleport'"}The caller may not use the signing key, or may not renew
The JSON body of every error response.
object
A stable, machine-readable kind.
What went wrong, for a person.
Example
{ "error": "invalid_request", "message": "unknown step 'teleport'"}The timestamp authority or a revocation source failed
The JSON body of every error response.
object
A stable, machine-readable kind.
What went wrong, for a person.
Example
{ "error": "invalid_request", "message": "unknown step 'teleport'"}Signature=certificate and no signing key is configured, the level or the renewal needs a timestamp authority that is not configured, or a certificate has expired or been revoked
The JSON body of every error response.
object
A stable, machine-readable kind.
What went wrong, for a person.
Example
{ "error": "invalid_request", "message": "unknown step 'teleport'"}