Skip to content

TypeScript SDK

llms.txtAsk agent

@originalpictures/sdk is a typed client for the public API. It is generated from the same contract as the API reference. It covers all 109 operations and can call the 107 consumer operations. The two provider-ingress callbacks, for the Clerk and Stripe webhooks, are typed but deliberately not callable.

  • ES modules only
  • Runs anywhere with a standard fetch: Node.js and browsers
  • One runtime dependency, @noble/hashes
import { OriginalPicturesClient } from "@originalpictures/sdk";
const client = new OriginalPicturesClient({
baseUrl: process.env.OP_API_BASE_URL ?? "https://dev-api.originalpictures.com",
apiKey: process.env.OP_API_KEY,
timeoutMs: 30_000,
});

baseUrl can include a path prefix. A base URL with a query string or fragment throws ConfigurationError before any request is sent. Never ship an API key to a browser. In browsers, use the client only with a backend that holds the key.

Every executable operation is a method on client.operations, named by its operationId. Examples are createUpload, completeUpload, acceptSigningJob, getJob, createVerification, and listRecipes. Each method takes { body, parameters, headers, signal } and returns the typed response. client.request(operationId, options) is the same call made by name.

import { createReadStream, readFileSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { idempotencyKey, poll, signJobTerminal } from "@originalpictures/sdk";
const path = "photo.jpg";
const sha256 = createHash("sha256").update(readFileSync(path)).digest("hex");
const byteLength = statSync(path).size;
const upload = await client.operations.createUpload({
body: { byte_length: byteLength, sha256, declared_media_type: "image/jpeg", mode: "single" },
});
// Sends exactly the issued headers, never your API key, and checks length and SHA-256.
await client.uploadNodeStream(upload.upload!.url, createReadStream(path), {
byteLength,
sha256,
headers: upload.upload!.headers as Record<string, string>,
});
await client.operations.completeUpload({
body: {},
parameters: { upload_id: upload.upload_id },
headers: { "Idempotency-Key": idempotencyKey() },
});
const accepted = await client.operations.acceptSigningJob({
body: { asset_id: upload.asset_id, recipe_id: "rcp_system_sign_only" },
headers: { "Idempotency-Key": idempotencyKey() },
});
const job = await poll(
(signal) => client.operations.getJob({ parameters: { job_id: accepted.job_id }, signal }),
signJobTerminal,
{ timeoutMs: 300_000 },
);
console.log(job.state, job.result);

The three signing modes are rcp_system_sign (C2PA only), rcp_system_watermark_then_sign_unfingerprinted (watermark, no fingerprint) and rcp_system_watermark_then_sign (watermark and fingerprints); see Choose a mode. watermark_strength is subtle, balanced (the default) or robust:

const subtle = await client.operations.acceptSigningJob({
body: {
asset_id: upload.asset_id,
recipe_id: "rcp_system_watermark_then_sign_unfingerprinted",
watermark_strength: "subtle",
},
headers: { "Idempotency-Key": idempotencyKey() },
});

A key whose signing profile names a default recipe may omit recipe_id; its other defaults fill the fields a request omits.

In browsers, use uploadBlob instead of uploadNodeStream.

signJobTerminal fails closed on inconsistent snapshots. For example, a completed job must carry ready vfy_… evidence. For verifications, use createVerification with verificationJobTerminal. A vjob_… ID is only a pollable job, never evidence.

poll(fetch, done, { timeoutMs, intervalMs, maxAttempts, signal }) backs off from intervalMs up to 5 seconds. Your fetch callback receives an AbortSignal. A deadline or caller cancellation settles the poll immediately and aborts that signal, so the underlying request is released.

client.download(url, { byteLength, sha256, sink }) streams into a staged sink. sink.commit() runs only after the exact length and SHA-256 match. On any failure, sink.abort() runs and the sink is never committed.

verifyWebhook(rawBody, { secret, eventId, timestamp, signature }) checks a delivery against the exact raw bytes. See Webhooks.

API failures throw subclasses of ApiError: AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, ValidationError, RateLimitError, and ServiceUnavailableError. Each carries statusCode, code, requestId, retryable, fieldDetails, and documentationUrl. Transport problems throw TransportError or DeadlineExceededError, and integrity failures throw IntegrityError. All of them derive from OriginalPicturesError. See Errors.