Signing a PDF with your own certificate, and verifying one

Sign takes the certificate and key you supply; verify checks a document against a certificate. What a cryptographic signature proves, and what it doesn't.

POST /v1/pdf/sign on Infrai applies a cryptographic signature using a cert_pem and key_pem that you supply, and POST /v1/pdf/verify checks a signed document against a certificate. That’s a different thing from an e-signature product: there’s no signing ceremony, no email invitation, no audit trail of who clicked where — this is the cryptographic primitive, and you bring the key material.

Useful when the signer is your own system. Not what you want when the signer is a customer.

Sign

curl -sS -X POST "https://api.infrai.cc/v1/pdf/sign" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "pdf": "pdf_2fVc8nRqLmT4xBzY",
    "cert_pem": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----",
    "key_pem": "-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----",
    "reason": "Issued statement, September 2026",
    "location": "Bristol, UK",
    "store": true
  }'
{
  "ok": true,
  "data": {
    "pdf_id": "pdf_9wQ1zV6pLkS3dHyB",
    "url": "https://files.infrai.cc/pdf/9wQ1zV6pLkS3dHyB.pdf",
    "size_bytes": 194560,
    "page_count": 4,
    "sha256": "9f2c41bd7a9e8c0b5d3e1f7a2b8d4c6e0a9f3b1d5c7e2a4f6b8d0c2e4a6f8b0d",
    "signed": true,
    "signer_common_name": "Northwind Statements",
    "signed_at": "2026-09-21T03:45:12Z",
    "reason": "Issued statement, September 2026",
    "location": "Bristol, UK",
    "retention_days": 7
  }
}

signer_common_name comes from the certificate, so it’s worth checking it matches what you expect — a signature applied with the wrong certificate is a valid signature by the wrong entity, which verifies fine and means nothing.

reason and location are embedded in the signature and shown by PDF readers. Put something specific in reason; “Signed” tells a reader nothing they didn’t already know.

Verify one you were sent

curl -sS -X POST "https://api.infrai.cc/v1/pdf/verify" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "signed_pdf": "https://files.example.com/inbox/countersigned-4821.pdf",
    "cert_pem": "-----BEGIN CERTIFICATE-----\nMIID...\n-----END CERTIFICATE-----"
  }'
{
  "ok": true,
  "data": {
    "valid": true,
    "signatures": [
      {"signer_common_name": "Northwind Statements", "signed_at": "2026-09-21T03:45:12Z",
       "reason": "Issued statement, September 2026", "covers_whole_document": true}
    ]
  }
}

valid is the headline and signatures is where the detail lives. Read both: a document can carry a valid signature that covers only part of it, which is how a page gets appended after signing without invalidating what was signed.

Keep the key out of the request path

Passing key_pem in a request body means your private key is in memory, in a log if you’re careless, and in whatever traces your framework keeps. Load it from a secret store at the last moment and never let it near a log line.

import os

import requests

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})


def _key_material() -> tuple[str, str]:
    """Read at the point of use. Holding the private key in a module-level constant
    means it lands in every heap dump and every debugger session for the life of
    the process."""
    cert = os.environ["SIGNING_CERT_PEM"]
    key = os.environ["SIGNING_KEY_PEM"]
    if "PRIVATE KEY" not in key:
        raise RuntimeError("SIGNING_KEY_PEM does not look like a private key")
    return cert, key


def sign(pdf_id: str, reason: str, location: str, expected_cn: str) -> dict:
    cert, key = _key_material()
    resp = SESSION.post(
        f"{API}/v1/pdf/sign",
        json={"pdf": pdf_id, "cert_pem": cert, "key_pem": key,
              "reason": reason, "location": location, "store": True},
        timeout=180,
    )
    body = resp.json()
    if not body.get("ok"):
        # Never echo the request body into an error: it contains the private key.
        raise RuntimeError(f"sign failed: {body.get('error', {}).get('code')}")
    data = body["data"]
    if not data.get("signed"):
        raise RuntimeError("response did not report a signature")
    if data.get("signer_common_name") != expected_cn:
        raise RuntimeError(f"signed as {data.get('signer_common_name')}, expected {expected_cn}")
    return {"pdf_id": data["pdf_id"], "url": data["url"], "sha256": data["sha256"],
            "signed_at": data["signed_at"]}


def verify(signed_pdf: str, cert_pem: str) -> dict:
    resp = SESSION.post(f"{API}/v1/pdf/verify",
                        json={"signed_pdf": signed_pdf, "cert_pem": cert_pem}, timeout=120)
    resp.raise_for_status()
    data = resp.json()["data"]
    whole = all(s.get("covers_whole_document", False) for s in data.get("signatures", []))
    return {"valid": bool(data.get("valid")) and whole,
            "signature_count": len(data.get("signatures", [])),
            "covers_whole_document": whole}


if __name__ == "__main__":
    signed = sign(os.environ["PDF_ID"], "Issued statement, September 2026",
                  "Bristol, UK", expected_cn="Northwind Statements")
    print(signed)

Requiring covers_whole_document in your own verify wrapper is the check most integrations miss, and it’s the one that catches a document extended after signing.

Sign last, and archive what you signed

OrderResult
generate → signthe signature covers the finished document
sign → watermarkthe watermark invalidates or partially covers the signature
sign → compressre-encoding can break the signature
sign → archivecorrect: the archived bytes are the signed bytes

Signing is the last operation. Anything that rewrites the file afterwards changes the bytes the signature covers, and the signature stops validating — which is the behaviour you want from a signature and a surprise if you put compression after it in a pipeline.

Archive immediately with PUT /v1/storage/object/put/{bucket}/{key} and store the sha256. The signed document is the record; the platform’s retention_days is a working window.

What a signature proves

It proves the document hasn’t changed since it was signed, and that whoever signed it held the private key. It does not prove who a human is, that they read the document, that they agreed to it, or when they saw it — those are the things an e-signature service establishes with identity verification, a signing ceremony and an audit trail.

So this is the right tool for one claim and the wrong tool for another.

“Our system issued this document and it has not been altered since” is exactly what a certificate-based signature establishes, and it is a genuinely useful thing to be able to demonstrate about a statement, a certificate of insurance or an audit report. “The customer read and agreed to these terms on this date” is a different claim entirely, resting on identity verification and a recorded act of consent that no cryptographic operation on a file can supply.

Limitations

You bring the certificate, which means obtaining one from a certificate authority and managing its expiry and rotation — there’s no key management here. Timestamping from a trusted authority isn’t part of this surface either, so a signature’s date rests on signed_at rather than on an independent timestamp token, which matters for long-term validation.

And there’s no signature workflow: no invitation, no multi-party sequencing, no completion webhook. Anvil and dedicated e-signature platforms exist for exactly that, and if a customer is the signer you should be using one rather than assembling the experience from this endpoint.

What one credential gives you is the chain: generate, sign, archive, deliver by POST /v1/email/send, and one GET /v1/account/usage covering all four. Signing and verification bill per call, live in GET /v1/discovery/pdf.sign (verified 2026-09-21), with rates drifting downward as vendor contracts improve.

References

Browse more pdf developer guides