{"_id":"@capseal/sdk","name":"@capseal/sdk","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@capseal/sdk","version":"0.1.0","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"description":"Verify where a photo, video or document came from. Reads C2PA manifests, EXIF and XMP, and tells you how it knows - never a confidence score it cannot justify.","keywords":["c2pa","provenance","content-credentials","ai-detection","deepfake","image-forensics","exif","xmp","verification","capseal"],"license":"SEE LICENSE IN LICENSE","author":{"name":"SYPTime Pty Ltd"},"homepage":"https://capseal.ai/developers","repository":{"type":"git","url":"git+https://github.com/CapSeal-ai/sdk.git"},"bugs":{"url":"https://github.com/CapSeal-ai/sdk/issues"},"publishConfig":{"access":"public"},"engines":{"node":">=18"},"scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","test":"vitest run"},"devDependencies":{"typescript":"^5.7.2","vitest":"^2.1.8"},"_id":"@capseal/sdk@0.1.0","gitHead":"27a76fc504de60e45017691cf78e299bce424a42","_nodeVersion":"23.8.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-zqZH46mugIt+QOdZwuCrqC0wxquv1RczSl0VZPHM+4H6yJ7Dm4IX8oRV3CKQg7sdR5FcfpwvytTOUR9aClj98A==","shasum":"df0b1591ce81d9079de4ed30e29bd406a218107e","tarball":"https://registry.npmjs.org/@capseal/sdk/-/sdk-0.1.0.tgz","fileCount":7,"unpackedSize":35436,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDcmSZdY2OIQ94Z9YdiQzyb2cXaFy4BHzwv4NoWHnUDoAIgEr4GrvRiErWcBC+yGF3uxmdcDsbnZBvycvglD4FVjgY="}]},"_npmUser":{"name":"uozef","email":"yousef.hosseini@gmail.com"},"directories":{},"maintainers":[{"name":"uozef","email":"yousef.hosseini@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_0.1.0_1786358047671_0.19710301094299254"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-10T10:34:07.445Z","0.1.0":"2026-08-10T10:34:07.819Z","modified":"2026-08-10T10:34:08.097Z"},"maintainers":[{"name":"uozef","email":"yousef.hosseini@gmail.com"}],"description":"Verify where a photo, video or document came from. Reads C2PA manifests, EXIF and XMP, and tells you how it knows - never a confidence score it cannot justify.","homepage":"https://capseal.ai/developers","keywords":["c2pa","provenance","content-credentials","ai-detection","deepfake","image-forensics","exif","xmp","verification","capseal"],"repository":{"type":"git","url":"git+https://github.com/CapSeal-ai/sdk.git"},"author":{"name":"SYPTime Pty Ltd"},"bugs":{"url":"https://github.com/CapSeal-ai/sdk/issues"},"license":"SEE LICENSE IN LICENSE","readme":"<div align=\"center\">\n\n# CapSeal\n\n**Find out where a photo, video or document actually came from.**\n\nC2PA manifests, EXIF, XMP — read in one call, with an answer that tells you *how* it knows.\n\n[![npm](https://img.shields.io/npm/v/@capseal/sdk?color=27E1A4&label=%40capseal%2Fsdk)](https://www.npmjs.com/package/@capseal/sdk)\n[![node](https://img.shields.io/badge/node-%E2%89%A518-27E1A4)](https://nodejs.org)\n[![types](https://img.shields.io/badge/types-included-27E1A4)](#reference)\n\n[Try it now, no signup](https://capseal.ai/check) · [Free API key](https://capseal.ai/account) · [Docs](https://capseal.ai/developers)\n\n</div>\n\n---\n\n```bash\nnpm i @capseal/sdk\n```\n\n```ts\nimport { CapSeal } from \"@capseal/sdk\";\nimport { readFileSync } from \"node:fs\";\n\nconst capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });\nconst { verdict } = await capseal.inspect(readFileSync(\"photo.jpg\"));\n\nconsole.log(verdict.verdict);  // \"AI_GENERATED\"\nconsole.log(verdict.basis);    // \"declared\"\nconsole.log(verdict.headline); // \"This file says it was generated by Adobe Firefly.\"\n```\n\nThat's the whole integration. [Get a key](https://capseal.ai/account) — free, instant, no card.\n\n---\n\n## Why you need this\n\nYou accept files from people. A claims photo, a KYC selfie, an insurance\ninspection, a marketplace listing, a signed PDF, a piece of user-generated\ncontent. Until recently you could assume that a photograph was a photograph.\n\nThat assumption is gone. Anyone can generate a convincing image of damage that\nnever happened, in seconds, for free. Meanwhile the countermeasure is arriving:\ncameras, phones and the major generative tools are all shipping **C2PA Content\nCredentials** — a signed record, embedded in the file, of where it came from and\nwhat was done to it.\n\nThe problem is that reading it correctly is fiddly and most implementations get\nit wrong. Manifests span multiple JPEG segments. Assertions are CBOR inside JUMBF\nboxes. The interesting facts are scattered across the manifest, EXIF and XMP, and\nthey contradict each other in informative ways.\n\n**This SDK is one call that reads all of it and gives you a straight answer.**\n\n### What makes the answer trustworthy\n\nMost tools in this space return a percentage. `0.87 likely AI`. That number is\nalmost always invented, and it fails in the direction that hurts: a screenshot of\na real photograph gets flagged, a generated image that has been re-encoded gets\nwaved through.\n\nCapSeal returns a **basis** — how the answer was reached:\n\n| `basis` | what happened | automate on it? |\n| --- | --- | --- |\n| `proven` | cryptography settles it | **yes** |\n| `declared` | the file says so, and a signature backs the file | **yes** |\n| `claimed` | the file says so, nothing backs the claim | no — review |\n| `indicative` | metadata leans that way, could be wrong | no — review |\n| `unknown` | the file does not say, and neither will we | no — review |\n\n```ts\nconst { verdict } = await capseal.inspect(bytes);\n\nif (verdict.basis === \"proven\" || verdict.basis === \"declared\") {\n  act(verdict.verdict);         // safe to route automatically\n} else {\n  review(verdict.headline);     // a person should look\n}\n```\n\nBuild on `basis` and your logic stays correct as detection improves, because you\nnever encoded a guess as a fact.\n\n### Why there is no \"is it AI?\" score\n\nAn unsigned JPEG with no metadata is a grid of pixels. Nothing in those bytes\ndistinguishes:\n\n- a genuine photograph that WhatsApp stripped the metadata from\n- a lightly edited photograph\n- a diffusion model's output\n\nAnything claiming otherwise is guessing, and defeating it costs an attacker one\nscreenshot. So for that case you get `NO_PROVENANCE` / `unknown` and a sentence\nexplaining why — which is less satisfying and considerably more useful, because\nyou can build on it.\n\nWhen a file *does* carry provenance — and an increasing share do — you get a\ndefinite answer with the receipts attached.\n\n---\n\n## Quick start\n\n**1. Get a key** at [capseal.ai/account](https://capseal.ai/account). Free, no\ncard, 1,000 verifications a day.\n\n**2. Install and call.**\n\n```bash\nnpm i @capseal/sdk\nexport CAPSEAL_API_KEY=csk_live_...\n```\n\n```ts\nimport { CapSeal } from \"@capseal/sdk\";\n\nconst capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });\nconst result = await capseal.inspect(fileBytes);\n```\n\n**3. Or try it first with no signup at all:**\n\n```bash\ncurl -X POST --data-binary @photo.jpg https://capseal.ai/api/demo/inspect\n```\n\n---\n\n## Examples\n\nRunnable versions of everything below are in [`examples/`](./examples).\n\n### Node — the basics\n\n```ts\nimport { CapSeal } from \"@capseal/sdk\";\nimport { readFileSync } from \"node:fs\";\n\nconst capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });\nconst { verdict, report } = await capseal.inspect(readFileSync(\"evidence.jpg\"));\n\nconsole.log(verdict.headline);\nconsole.log(`basis: ${verdict.basis}`);\n\nif (report.c2pa.present) {\n  const m = report.c2pa.activeManifest;\n  console.log(`signed by  ${m?.claimGenerator}`);\n  console.log(`algorithm  ${m?.signatureAlgorithm}`);\n  console.log(`actions    ${m?.actions.map((a) => a.action).join(\", \")}`);\n}\n\nif (report.exif?.make) {\n  console.log(`camera     ${report.exif.make} ${report.exif.model}`);\n}\n```\n\n### Express — gate an upload\n\n```ts\nimport express from \"express\";\nimport { CapSeal } from \"@capseal/sdk\";\n\nconst capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY });\nconst app = express();\n\napp.post(\"/claims/:id/evidence\",\n  express.raw({ type: \"*/*\", limit: \"25mb\" }),\n  async (req, res) => {\n    const { verdict, report } = await capseal.inspect(req.body);\n\n    // Refuse only what is provably wrong. Everything uncertain goes to a human,\n    // because rejecting an honest customer costs more than a review does.\n    if (verdict.verdict === \"AI_GENERATED\" && verdict.basis === \"declared\") {\n      return res.status(422).json({\n        accepted: false,\n        reason: verdict.headline,\n        evidence: verdict.supportedBy,\n      });\n    }\n\n    await store(req.params.id, req.body, {\n      sha256: report.file.sha256,\n      verdict: verdict.verdict,\n      basis: verdict.basis,\n      needsReview: verdict.basis !== \"declared\" && verdict.basis !== \"proven\",\n    });\n\n    res.json({ accepted: true, verdict: verdict.verdict, basis: verdict.basis });\n  });\n```\n\n### Next.js — a route handler\n\n```ts\n// app/api/verify/route.ts\nimport { CapSeal } from \"@capseal/sdk\";\nimport { NextResponse } from \"next/server\";\n\nconst capseal = new CapSeal({ apiKey: process.env.CAPSEAL_API_KEY! });\n\nexport async function POST(request: Request) {\n  const form = await request.formData();\n  const file = form.get(\"file\");\n  if (!(file instanceof Blob)) {\n    return NextResponse.json({ error: \"no file\" }, { status: 400 });\n  }\n\n  const { verdict, report } = await capseal.inspect(file);\n  return NextResponse.json({\n    verdict: verdict.verdict,\n    basis: verdict.basis,\n    headline: verdict.headline,\n    sha256: report.file.sha256,\n  });\n}\n```\n\n### Browser — never put the key in the client\n\n```ts\n// The key belongs on your server. This posts to your own endpoint, which calls\n// CapSeal. A key shipped to a browser is a key you have given away.\nconst response = await fetch(\"/api/verify\", { method: \"POST\", body: file });\nconst { verdict, basis, headline } = await response.json();\n\ndocument.querySelector(\"#result\").textContent =\n  basis === \"declared\" || basis === \"proven\"\n    ? headline\n    : `${headline} (needs a human: ${basis})`;\n```\n\n### Cloudflare Workers\n\n```ts\nimport { CapSeal } from \"@capseal/sdk\";\n\nexport default {\n  async fetch(request: Request, env: { CAPSEAL_API_KEY: string }) {\n    const capseal = new CapSeal({ apiKey: env.CAPSEAL_API_KEY });\n    const { verdict } = await capseal.inspect(await request.arrayBuffer());\n    return Response.json(verdict);\n  },\n};\n```\n\n### Handling every verdict\n\n```ts\nconst { verdict } = await capseal.inspect(bytes);\n\nswitch (verdict.verdict) {\n  case \"SEALED_BY_CAPSEAL\":\n    // Captured through a CapSeal SDK. Integrity signals were measured on the\n    // device at the moment of capture.\n    break;\n  case \"AI_GENERATED\":\n    // A generator declared itself, in a signed manifest or in XMP.\n    break;\n  case \"EDITED\":\n    // The file records edits: crops, colour work, composites.\n    break;\n  case \"PROVENANCE_PRESENT\":\n    // C2PA provenance from a camera or tool, nothing notable declared.\n    break;\n  case \"CAMERA_ORIGINAL_LIKELY\":\n    // MakerNote, capture timestamp, no editor named. Indicative, not proof.\n    break;\n  case \"NO_PROVENANCE\":\n    // Nothing to go on. Common and not suspicious on its own.\n    break;\n}\n```\n\n### Verify a proof with no key and no network\n\nIf someone hands you a CapSeal proof object, checking it *through our API* means\ntrusting us about our own work. Don't.\n\n```bash\nnpm i @capseal/wasm\n```\n\n```ts\nimport { loadCapseal } from \"@capseal/wasm\";\nimport { readFileSync } from \"node:fs\";\n\nconst capseal = await loadCapseal(\n  readFileSync(\"node_modules/@capseal/wasm/capseal.wasm\"),\n);\n\nconst result = capseal.replay({ signedProof, proofPublicKeyBase64, assetBase64 });\n\nresult.signatureValid    // we issued it, unedited\nresult.reasoningReplays  // the conclusions follow from the checks\nresult.evidenceReplays   // the checks follow from the file\nresult.trustworthy\n```\n\nThat module **imports nothing** — it cannot open a socket or read your disk, and\nyou can confirm that in one line rather than take our word for it:\n\n```js\nWebAssembly.Module.imports(new WebAssembly.Module(bytes));  // []\n```\n\nIt exists precisely so a verdict never depends on our goodwill or our uptime.\n\n---\n\n## What it reads\n\n**C2PA / Content Credentials** — the full manifest: claim generator, every\nassertion, actions, ingredients, signature algorithm. Multi-segment manifests are\nreassembled correctly (a manifest routinely exceeds JPEG's 64KB segment limit,\nand readers that miss this report valid files as corrupt).\n\n**EXIF** — every IFD0, ExifIFD and GPS tag, with the provenance-relevant ones\nsurfaced: camera make and model, MakerNote presence (editors usually destroy it),\nSoftware, capture and modify times, coordinates.\n\n**XMP** — CreatorTool, edit history, and the IPTC `digitalSourceType` vocabulary,\nwhich is how a generative model declares itself.\n\n**PDF** — Producer and Creator, plus the count of appended revisions, which is\ndirect evidence of modification after writing.\n\n**Containers** — JPEG, PNG, WebP, GIF, TIFF, HEIC, AVIF, MP4, QuickTime, WebM,\nPDF, SVG. Detected by magic bytes, never by the filename you were given.\n\n### The full report\n\n`verdict` is the answer. `report` is everything it was based on — so if you\ndisagree, the material to argue with is in the same response.\n\n```ts\nreport.file.sha256                        // ties the result to exact bytes\nreport.file.container                     // \"jpeg\"\nreport.c2pa.activeManifest?.claimGenerator\nreport.c2pa.activeManifest?.actions       // [{ action: \"c2pa.color_adjustments\" }]\nreport.c2pa.activeManifest?.assertions    // every one, with size and kind\nreport.exif                               // every tag\nreport.xmp                                // parsed, plus the raw packet\nreport.evidence                           // each observation, with its source\nreport.warnings                           // anything that failed to parse\n```\n\n---\n\n## Errors you can act on\n\n```ts\nimport { CapSealError } from \"@capseal/sdk\";\n\ntry {\n  await capseal.inspect(bytes);\n} catch (error) {\n  if (error instanceof CapSealError) {\n    error.status;    // 401, 413, 429 ...\n    error.retryable; // true on 429\n    error.message;   // \"That API key was revoked. Issue a new one at ...\"\n  }\n}\n```\n\nThe platform distinguishes *not recognised* from *revoked* from *over quota*, and\nthe SDK passes that through rather than collapsing it into \"request failed\".\n\nWatch your quota without guessing:\n\n```ts\nconst { quota } = await capseal.inspect(bytes);\nif (quota && quota.remaining < 50) warnOperations();\n```\n\n---\n\n## Reference\n\n```ts\nnew CapSeal({\n  apiKey: string,        // required — https://capseal.ai/account\n  baseUrl?: string,      // defaults to https://capseal.ai\n  fetch?: typeof fetch,  // for tests or a proxy\n  timeoutMs?: number,    // defaults to 30000\n});\n```\n\n| method | does |\n| --- | --- |\n| `inspect(file)` | read a file's provenance. Takes `Uint8Array`, `ArrayBuffer`, `Buffer` or `Blob` |\n| `replay(proof)` | re-check a proof object we issued |\n| `capabilities()` | what this deployment does, and what it deliberately will not |\n\nNode 18+, Deno, Bun, browsers, Cloudflare Workers. Types included. No runtime\ndependencies.\n\n---\n\n## FAQ\n\n**Can you tell me if an image is AI-generated?**\nIf the generator declared it — which Firefly, and a growing number of others, do\nvia C2PA — then yes, definitively, with the manifest as evidence. If it didn't,\nthen no, and neither can anyone else reliably. We say so rather than guess.\n\n**What about a photo with the metadata stripped?**\n`NO_PROVENANCE` / `unknown`. That is the honest answer: stripping is what every\nmessaging app does to perfectly genuine photographs, so its absence tells you\nalmost nothing. Route those to a human.\n\n**Do you store my files?**\nNo. Files are read in memory and discarded. The response says so explicitly in\n`retention`.\n\n**Is the SDK open source?**\nYes — [github.com/CapSeal-ai/sdk](https://github.com/CapSeal-ai/sdk). Verification\nruns on the platform, which is what makes the API key meaningful. If you want\nverification that runs entirely on your machine, that is\n[`@capseal/wasm`](https://www.npmjs.com/package/@capseal/wasm), and it needs no\nkey at all.\n\n**How do I stop this happening in the first place?**\nRead provenance and you are always reacting. Seal at *capture* and you are not:\nthe CapSeal capture SDKs for iOS and Android measure integrity signals on the\ndevice — parallax against the IMU, depth relief, screen-replay detection,\nhardware attestation — and bind them into the manifest at the moment the shutter\nfires. That turns `unknown` into `proven`. See\n[capseal.ai/platform](https://capseal.ai/platform).\n\n---\n\n## Limits\n\n- 25 MB per file\n- 1,000 calls a day on the free tier\n- Nothing stored\n\n---\n\n<div align=\"center\">\n\nBy [SYPTime Pty Ltd](https://capseal.ai) ·\n[Issues](https://github.com/CapSeal-ai/sdk/issues) ·\n[capseal.ai](https://capseal.ai)\n\n</div>\n","readmeFilename":"README.md","_rev":"1-2318377c78b1e0cfb8e8aacc6ea1110b"}