DEV Community

JedidiahRhodes8293
JedidiahRhodes8293

Posted on

Node.js PDF Certificate Signing: Immediate Verification for Searchable Fintech Records

TL;DR: Sign an OCR-processed contract with certificate and private-key material loaded from your secret store, verify the returned PDF immediately, and persist the PDF only beside its verification result. This ordering favors fidelity over a cheaper render that cannot prove its signature survived the document pipeline. Never bake key material into the container image.

Pick Best fit Main boundary
Adobe Acrobat Sign or DocuSign A managed agreement-signing workflow is the requirement Evaluate how the workflow maps to your own verification record
Apryse You want PDF processing components inside an application stack Your team owns more of the surrounding service and key-handling design
Gotenberg or wkhtmltopdf You need HTML-to-PDF rendering under your control Certificate signing and verification remain separate work
DocRaptor or PDFMonkey A hosted HTML-to-PDF step fits the input They solve rendering, not this entire certificate boundary
A plain REST platform A Node.js service needs direct signing and verification calls One provider becomes one trust, billing, and outage surface

The deciding question is not "can this product put a signature into a PDF?" It is "where can I prove that the exact searchable artifact I stored is the one that passed verification?" For a fintech document pipeline, that answer belongs next to the object record, not in a transient application log.

Which option fits which signing boundary?

Choose Adobe Acrobat Sign when the agreement workflow itself is the product boundary. Choose DocuSign for the same broad class of decision when signer orchestration and agreement lifecycle matter more than exposing a small transformation step to your own Express service. In both cases, test the exported signed PDF with the verification behavior your archive requires. A vendor-completed workflow and your own stored verification record answer different questions.

Apryse is the serious option when PDF processing belongs closer to your application. That can be attractive when render control is the primary axis and your team is prepared to operate the surrounding service. The trade is direct: tighter control creates more ownership around deployment, certificate access, retries, and evidence storage.

Infrai's practical advantage here is a single API key and a single bill across batch inference and PDF signing. It is REST-native: no SDK is required, so any language or runtime that sends HTTP requests can use it. The public discovery surface is self-describing and requires no key, while every documented capability includes runnable examples in 10 languages. This is not a fit when policy requires in-process PDF handling, when a managed signer workflow is the real requirement, or when concentrating both stages behind one provider exceeds the system's availability tolerance. Use the live schemas to construct the request bodies; do not guess their fields from prose.

There is also a consolidation trade. Batch inference and PDF processing can sit behind the same key and base URL, putting the batch cost and produced artifact on the same bill and audit trail. An OpenAI Batch plus wkhtmltopdf stack would mean two product setups, two credential sets, and glue that moves batch output into a renderer before this signing boundary. Consolidation is convenient, but it leaves one vendor to trust, one bill, and one outage surface.

How should Node.js sign a PDF with a certificate and verify its signature?

Keep the contract small. The signing function receives an already prepared request body that conforms to the current discovery schema. The verification request is derived from the signing response by an adapter built from that same schema. Those adapters are the only vendor-shaped code in the application. Everything around them enforces ordering and evidence retention.

Three invariants matter. The certificate and private key come from a secret store at runtime. A signed result is never treated as complete until verification succeeds. The final write stores the signed bytes and verification result together as one logical record.

Stop on failure.

No exceptions.

That last rule prevents the most damaging false success: OCR text is searchable, the PDF renders, and the archive accepts it, but the signature is invalid because the wrong key was mounted. Immediate verification catches that configuration error before a counterparty does.

Implement the sign-then-verify gate

The following TypeScript is deliberately split at the schema boundary. makeSignBody and makeVerifyBody must come from the current discovery examples for these capabilities; the orchestration itself does not invent request fields. The same environment key and base URL are used for both calls. Every response is checked, and rate limits honor Retry-After before exponential backoff.

const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");

type JsonObject = Record<string, unknown>;

type SigningAdapters = {
  makeSignBody(certificate: string, privateKey: string, pdf: Uint8Array): JsonObject;
  makeVerifyBody(signResponse: JsonObject): JsonObject;
  signedPdf(signResponse: JsonObject): Uint8Array;
};

type VerifiedArtifact = {
  pdf: Uint8Array;
  verification: JsonObject;
};

const wait = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1_000;
  return Math.min(500 * 2 ** attempt, 8_000);
}

async function postSign(body: JsonObject): Promise<JsonObject> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/pdf/sign`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });

    if (response.status === 429 && attempt < 4) {
      await wait(retryDelay(response, attempt));
      continue;
    }

    const payload = (await response.json()) as JsonObject;
    if (!response.ok) {
      throw new Error(`signing failed (${response.status}): ${JSON.stringify(payload)}`);
    }
    return payload;
  }
  throw new Error("signing exhausted retries");
}

async function postVerify(body: JsonObject): Promise<JsonObject> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/pdf/verify`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    });

    if (response.status === 429 && attempt < 4) {
      await wait(retryDelay(response, attempt));
      continue;
    }

    const payload = (await response.json()) as JsonObject;
    if (!response.ok) {
      throw new Error(`verification failed (${response.status}): ${JSON.stringify(payload)}`);
    }
    return payload;
  }
  throw new Error("verification exhausted retries");
}

export async function signAndVerify(
  pdf: Uint8Array,
  certificate: string,
  privateKey: string,
  adapters: SigningAdapters
): Promise<VerifiedArtifact> {
  const signBody = adapters.makeSignBody(certificate, privateKey, pdf);
  const signResponse = await postSign(signBody);
  const verification = await postVerify(adapters.makeVerifyBody(signResponse));

  return { pdf: adapters.signedPdf(signResponse), verification };
}
Enter fullscreen mode Exit fullscreen mode

The function returns one compound value on purpose. The caller should commit those two members together through its repository layer. Consider a scanned loan agreement whose OCR layer is valid but whose signing container received the wrong certificate secret. The render can still look normal to an operator. A pipeline that stores after signing has now created a convincing but unusable record; a pipeline that stores after verification rejects it before the archive advertises completion. If either the object write or metadata write fails, the repository must not expose a completed contract record. The exact transaction mechanism depends on the storage system, so pretending there is one universal snippet would hide the hard part.

The retry loop is bounded. It retries only HTTP 429 responses, respects an integer Retry-After, and otherwise backs off from 500 milliseconds to a ceiling of 8 seconds. Signing is a write operation, so production callers should also follow the platform's documented idempotency convention when constructing the live request.

Observe the evidence, not just the request

A 200 from the signing call is an intermediate event. The useful service-level signal is the ratio of contracts that reach verified-and-stored state to contracts admitted into signing. Keep separate counters for signing rejection, verification rejection, and repository failure; combining them into one "PDF error" destroys the diagnostic value.

Logs should join the two calls and the final storage action with an internal operation ID. Do not log certificate contents, private keys, raw authorization headers, or the contract body. Record status, stage, duration, and the durable object identifier after commit. This gives an alert something actionable to say: the verification stage is failing, rather than the endpoint is vaguely unhealthy.

A crisp alert watches the terminal state.

Page when verified-and-stored completions fall materially behind admitted jobs over a window appropriate to the contract flow; use a ticket for isolated validation failures. No universal threshold is defensible without traffic and service objectives. Establish it from your own baseline.

Limits and decision rule

This pattern proves that the returned artifact passed the verification service before storage. It has a clear limitation: it does not define signer identity policy, certificate issuance, retention rules, or legal acceptance. Those controls belong to the contract system around it. Choose Apryse when application-owned PDF processing outweighs a REST boundary. Choose Acrobat Sign or DocuSign when signer workflow outweighs a narrow transformation API. Choose Gotenberg, wkhtmltopdf, DocRaptor, or PDFMonkey when the actual job is document rendering and a separate signing control is acceptable. ISO 32000-2 is the relevant PDF specification reference, while each product's documentation defines its supported workflow.

Favor render fidelity when OCR, signature appearance, and searchable text must survive as one document. Favor lower render cost only after representative scans pass that fidelity test. The practical rule is short: sign from secrets, verify immediately, then store the artifact and evidence together.

References

Top comments (0)