Preparing documents before upload
Verification documents are capped at 5 MB per file, and must be PDF, JPEG, or PNG. Images must be at least 200×200 px. A PDF longer than 20 pages is accepted and split for you, up to 200 pages — see Long PDFs.
Most files that hit the cap are phone photos. A modern phone camera writes 4–12 MB per shot at a resolution far beyond what a document reviewer needs, so shrinking one is nearly free: a passport photo at 2000 px on its long edge is comfortably readable and usually lands under 1 MB. You do not need to re-collect anything from your users — re-encode what you already have.
Seismic does not compress files for you. Images are stored and reviewed as the exact bytes you send, so the quality trade-off stays with you. So are PDFs, unless one needs splitting or padding — see Long PDFs.
What to aim for
| Setting | Target |
|---|---|
| Long edge | 2000 px |
| Format | JPEG for photos and scans, PNG only for screenshots of text |
| JPEG quality | 80–85 |
| Floor | Stop at 1000 px on the long edge, never below 200×200 |
The floor matters more than the ceiling. A file that squeezes under 5 MB but comes out pixelated fails verification on review, which costs days — far more than asking the user for a better original. If you can't get under the limit while staying above 1000 px, treat that as a bad source file rather than compressing harder.
Two more things that cause avoidable rejections:
- Apply EXIF orientation before you strip metadata. Phone photos record rotation as a tag, not in the pixels. Re-encoding without applying it first delivers a sideways ID.
- Keep the whole document in frame. Don't crop to "just the important part", rotate-and-flatten, or watermark. Reviewers check edges, corners, and security features.
iPhone HEIC files. HEIC isn't an accepted format, and it's what iPhones capture by default. If your upload runs in a browser, listing only the accepted types on the file input is usually enough — current iOS Safari hands your page a JPEG rather than the original HEIC:
<input type="file" accept="image/jpeg,image/png,application/pdf" />Verify this on the iOS versions you support. Off the web, convert HEIC to JPEG before the steps below.
In the browser
Runs where the photo already is, so the user never uploads the oversized original. Steps the long edge down, then quality, and gives up rather than crossing the floor.
const MAX_BYTES = 5 * 1024 * 1024;
const MAX_EDGE = 2000;
const MIN_EDGE = 1000;
const QUALITY_STEPS = [0.85, 0.75, 0.65];
async function encodeJpeg(bitmap: ImageBitmap, edge: number, quality: number): Promise<Blob> {
const scale = Math.min(1, edge / Math.max(bitmap.width, bitmap.height));
const canvas = document.createElement("canvas");
canvas.width = Math.round(bitmap.width * scale);
canvas.height = Math.round(bitmap.height * scale);
canvas.getContext("2d")!.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
return new Promise((resolve, reject) => {
canvas.toBlob(
(blob) => (blob ? resolve(blob) : reject(new Error("JPEG encode failed"))),
"image/jpeg",
quality,
);
});
}
export async function prepareForUpload(file: File): Promise<File> {
if (file.size <= MAX_BYTES) return file;
// createImageBitmap applies EXIF orientation, so the canvas copy is upright.
const bitmap = await createImageBitmap(file);
for (let edge = MAX_EDGE; edge >= MIN_EDGE; edge -= 500) {
for (const quality of QUALITY_STEPS) {
const blob = await encodeJpeg(bitmap, edge, quality);
if (blob.size <= MAX_BYTES) {
return new File([blob], file.name.replace(/\.\w+$/, ".jpg"), { type: "image/jpeg" });
}
}
}
throw new Error("Cannot reach 5 MB above a readable resolution — ask for a better original");
}Then upload the result as the file part of POST /v0/customers/:id/kyc/documents.
On a server
sharp for batch work — re-encoding files you already hold, or a proxy in front of your own upload endpoint.
import sharp from "sharp";
const MAX_BYTES = 5 * 1024 * 1024;
const ATTEMPTS = [
{ edge: 2000, quality: 85 },
{ edge: 2000, quality: 75 },
{ edge: 1500, quality: 75 },
{ edge: 1000, quality: 75 },
] as const;
export async function prepareForUpload(input: Buffer): Promise<Buffer> {
for (const { edge, quality } of ATTEMPTS) {
const out = await sharp(input)
// Bare .rotate() applies the EXIF orientation tag; .jpeg() then drops metadata.
.rotate()
.resize({ width: edge, height: edge, fit: "inside", withoutEnlargement: true })
.jpeg({ quality, mozjpeg: true })
.toBuffer();
if (out.length <= MAX_BYTES) return out;
}
throw new Error("Cannot reach 5 MB above a readable resolution — ask for a better original");
}On the command line
One-offs and backfills. -auto-orient must come before -strip, for the reason above.
# Images: cap the long edge at 2000 px, re-encode, drop metadata.
magick input.jpg -auto-orient -resize '2000x2000>' -quality 82 -strip output.jpg
# Scanned PDFs: downsample the images embedded in each page.
gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook \
-dNOPAUSE -dQUIET -dBATCH -sOutputFile=output.pdf input.pdf
# Check the result, and open it before you trust it.
ls -l output.pdfOversized PDFs
An oversized PDF is almost always a scan: one large photo per page wrapped in PDF. The Ghostscript command above targets exactly that, and /ebook (150 dpi) typically cuts a scan by 5–10× while keeping text legible. /screen (72 dpi) is smaller but often too soft for verification — check it by eye before using it.
Two cases where re-encoding is the wrong fix:
- A multi-page PDF where one page is the document. Export that page and upload it as a JPEG. Reviewers only need the page that carries the evidence.
- A PDF you generated yourself that's over 5 MB. It's carrying embedded images at full camera resolution. Downscale them at generation time instead — you'll get a better result than any post-hoc pass.
A PDF that's genuinely text and vectors is essentially never over 5 MB. If one is, something is embedded that you didn't intend.
Long PDFs
A PDF longer than 20 pages is split at upload rather than rejected, so a long constitutional document doesn't need trimming first:
- The first 20 pages are stored under the
kindyou sent, and that document is what the response returns. - Each further span of 20 pages is stored as an extra
otherdocument, named after the original —bylaws_2.pdf,bylaws_3.pdf, … — with adescriptionnaming the pages it holds.
The extra documents show up in the document list like any other upload, and are reviewed together with the first. Past 200 pages the upload is rejected with a 400: split it yourself and send the parts that carry the evidence.
Verification also requires every file to be over 10 KB, which a short PDF — including the tail of a split — can fall under. Rather than reject it, Seismic appends a page reading "This is a filler page to push this file past the 10KB minimum size for uploads." so the file clears the floor.
Before you upload
- Open the output and read it at 100% zoom. Every field a reviewer needs must be legible.
- Confirm the size: 5 MB is 5,242,880 bytes, measured on the decoded file, not on a base64 envelope.
- Confirm orientation, especially after a batch run.
Both document endpoints reject an oversized file with 413 before storing anything, so a file that's too large is safe to retry after re-encoding: