Assembling a KYC packet on Deno Deploy

작성자

카테고리:

← 피드로
DEV Community · PDFops · 2026-07-22 개발(SW)

PDFops

Compliance owns the KYC application template, not engineering — and they revise it without filing a ticket. Hardcode the field names once and the next template swap either breaks silently (a field goes unfilled) or throws unknown_field in production. The fix is to stop assuming the shape of a PDF you don’t control: ask it, at request time, what fields it actually has, fill whatever comes back, then merge the filled application with the applicant’s uploaded ID scan into one packet. Here’s the whole flow in ~45 lines on Deno Deploy, using the new pdfops-sdk instead of raw fetch calls.

Why inspect first

Every other example on this blog hardcodes a JSON object of field names — reasonable when the template PDF is a file you committed to your own repo. A KYC application template isn’t that. It’s owned by a compliance or ops team who edit it in Acrobat, rename a field, add a new disclosure checkbox, and re-upload it to wherever your app fetches it from — all without a code review on your side. Filling against a hardcoded key list against a template you don’t control is a bug waiting for the next revision.

POST /api/inspect answers “what fields does this PDF have right now?” — every AcroForm field’s name, type, and (for dropdowns/radios) its options, plus a ready-to-fill fillTemplate object keyed by name. Call it once per request against the live template, and the fill step only ever writes into fields that actually exist. A template revision changes what gets filled, not whether the request throws.

Install the SDK, get a key

Every earlier post here uses fetch and FormData directly against the HTTP API — that still works and always will. pdfops-sdk is a thin typed wrapper around the same endpoints: no dependencies, edge-safe (Workers, Vercel Edge, Deno, Bun, Node 18+, browsers), and it turns the multipart plumbing into three method calls.

npm install pdfops-sdk

Enter fullscreen mode Exit fullscreen mode

You need a key to run more than the anonymous trial. A free one costs nothing and needs no card — email in, key out (the key arrives by email, not in the response body, so inbox possession is the verification step):

import { PdfOps } from 'pdfops-sdk';

const trial = new PdfOps(); // no key: 100 requests/IP/month
await trial.signup('[email protected]'); // free key, 250/month, no card — check your inbox

Enter fullscreen mode Exit fullscreen mode

Once the key lands, wire it in and every call meters against your own quota instead of your IP’s:

const pdfops = new PdfOps({ apiKey: Deno.env.get('PDFOPS_API_KEY') });

Enter fullscreen mode Exit fullscreen mode

The Deno Deploy handler

The endpoint takes a multipart POST — the applicant’s data as a JSON field, their ID scan as a file — pulls the current application template, inspects it, fills only the fields it finds, and merges in the ID scan. No browser, no second runtime, ~45 lines.

// main.ts — Deno Deploy
import { PdfOps, PdfOpsError } from 'pdfops-sdk';

const pdfops = new PdfOps({ apiKey: Deno.env.get('PDFOPS_API_KEY') });

Deno.serve(async (req: Request) => {
  if (req.method !== 'POST') {
    return new Response('POST multipart/form-data: applicant, id_scan', { status: 405 });
  }

  const form = await req.formData();
  const idScan = form.get('id_scan');
  const applicantRaw = form.get('applicant');
  if (!(idScan instanceof File) || typeof applicantRaw !== 'string') {
    return new Response('expected fields "applicant" (JSON) and "id_scan" (file)', { status: 400 });
  }
  const applicant: Record<string, unknown> = JSON.parse(applicantRaw);

  // 1. Pull the CURRENT template — compliance swaps this file on their
  //    own schedule, so the fetch (not a bundled asset) is deliberate.
  const templateResp = await fetch(Deno.env.get('TEMPLATE_URL')!);
  const template = new Uint8Array(await templateResp.arrayBuffer());

  // 2. Ask the template what fields it has right now, instead of
  //    assuming a fixed shape.
  const { fillTemplate } = await pdfops.inspect(template);

  // 3. Map applicant data onto whatever fields actually exist. A field
  //    the template dropped is silently skipped; a field the applicant
  //    didn't send stays at the template's default.
  const values: Record<string, string> = {};
  for (const name of Object.keys(fillTemplate)) {
    if (name in applicant) values[name] = String(applicant[name]);
  }

  try {
    // 4. Fill the application.
    const filled = await pdfops.fillForm(template, values);

    // 5. Merge the filled application with the uploaded ID scan into
    //    one packet — page order follows array order.
    const idBytes = new Uint8Array(await idScan.arrayBuffer());
    const packet = await pdfops.merge([filled, idBytes]);

    return new Response(packet, { headers: { 'content-type': 'application/pdf' } });
  } catch (e) {
    if (e instanceof PdfOpsError) {
      return new Response(JSON.stringify({ error: e.code, details: e.message }), { status: e.status });
    }
    throw e;
  }
});

Enter fullscreen mode Exit fullscreen mode

deployctl deploy main.ts, set PDFOPS_API_KEY and TEMPLATE_URL as environment variables, and you have a live endpoint. The fillForm and merge calls both return Uint8Array PDF bytes — hand them to storage, a Response, or an email attachment as-is; nothing in this handler buffers more than one document at a time.

Why merge goes last, and what else it can hold

merge([filled, idBytes]) concatenates pages in array order, so the filled application always lands as page 1 — the reviewer opens the packet and reads the application first, attachments after. The array isn’t capped at two: a real KYC packet is usually merge([filled, idFront, idBack, proofOfAddress]) — every attachment the applicant uploaded, in the order you want a reviewer to see them, still one POST /api/merge call.

Errors and watching your quota

Every non-2xx response the SDK sees throws a PdfOpsError with the API’s stable error slug — the catch block above turns unknown_field, invalid_pdf, or a 429 rate_limited straight into the right HTTP status for the caller, instead of a generic 500. And because the free tier has a real ceiling (250/month per key), check it before you’re surprised by one:

const { used, remaining, resets_at } = await pdfops.usage();

Enter fullscreen mode Exit fullscreen mode

That’s GET /api/usage under the hood — it reads the same counter the 429 fires on, so the number you show a user is never out of sync with the number that blocks them.

Where this fits a real app

Swap the runtime and the trigger, and the same three-call shape — inspect, fill, merge — covers most “combine a filled form with something a user uploaded” flows:

The part that’s new here is inspect in the chain — reach for it whenever the template isn’t a file you fully control, not just for KYC packets.

Try it

All three endpoints are live. Prove the chain from your terminal before writing a line of Deno:

# 1. What fields does this template have?
curl -X POST https://pdfops.dev/api/inspect -F "[email protected]"

# 2. Fill it
curl -X POST https://pdfops.dev/api/fill-form \
  -F "[email protected]" \
  -F 'fields={"applicant_name":"Ada Lovelace","country":"UK"}' \
  -o filled.pdf

# 3. Merge with the ID scan
curl -X POST https://pdfops.dev/api/merge \
  -F "[email protected]" -F "[email protected]" \
  -o packet.pdf

Enter fullscreen mode Exit fullscreen mode

You get 100 keyless requests per IP per month, or pdfops.signup('[email protected]') / the form at /pricing for a free key (250/mo, no card — delivered by email).

Building a KYC or onboarding flow and something about inspect‘s field-type coverage doesn’t fit yours? Drop a note on the feedback form — it’s the fastest way to influence what ships next.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다