@vitra/image-creator-node
v1.0.0-rc.5
Published
Node.js SDK for the Vitra Image Creation Platform — HMAC webhook helpers, credit ledger, session tokens.
Maintainers
Readme
@vitra/image-creator-node
Node.js SDK for the Vitra Image Creation Platform. Ships the VitraClient
for outbound calls and signWebhook / verifyWebhook helpers for the HMAC
webhook contract.
Works with Node 18+ (native fetch), Next.js App Router, Express, Hono, etc.
Install
npm install @vitra/image-creator-node zodzod is a peer dep so you can share a single zod version across your app.
Quick start — 20 lines of Translate.photo integration
// app/api/vitra/session/route.ts (Next.js App Router)
import { VitraClient } from "@vitra/image-creator-node";
import { auth } from "@/lib/auth";
const vitra = new VitraClient({
appId: "translate-photo",
sharedSecret: process.env.VITRA_IC_SECRET!,
});
export async function POST(req: Request) {
const user = await auth(req);
const session = await vitra.session.create({
userId: user.id,
parentOrigin: "https://app.translate.photo",
});
return Response.json(session);
}That's the whole integration for session creation. On the frontend, pair it
with @vitra/image-creator-react (see that package's README).
Receiving credit webhooks
The Image Creator signs every request to your credit endpoints. Verify the signature against the raw body before parsing JSON:
// app/api/credits/reserve/route.ts
import {
verifyWebhook,
creditReservePayloadSchema,
} from "@vitra/image-creator-node";
import { reserveCreditsInLedger } from "@/lib/credits";
export async function POST(req: Request) {
const raw = await req.text(); // ← read RAW body first; do NOT req.json()
const result = verifyWebhook({
header: req.headers.get("x-vitra-signature") ?? undefined,
body: raw,
sharedSecret: process.env.VITRA_IC_SECRET!,
});
if (!result.valid) return new Response("unauthorized", { status: 401 });
const payload = creditReservePayloadSchema.parse(JSON.parse(raw));
const reservation = await reserveCreditsInLedger(payload); // idempotent on job_id
if (reservation.status === "insufficient") {
return Response.json({ error: "insufficient_credits" }, { status: 402 });
}
return Response.json({ reservation_id: reservation.id });
}Same pattern for /credits/capture and /credits/release. All three
endpoints are idempotent on job_id + reservation_id.
VitraClient
Construction
const vitra = new VitraClient({
appId: "translate-photo",
sharedSecret: process.env.VITRA_IC_SECRET!, // from admin dashboard
baseUrl: "https://image-creator.vitra.ai", // default; override for staging
timeoutMs: 5000, // default
});The client is stateless and safe to create once per process. Reuse the same instance across requests — there's no connection state.
vitra.session.create(args)
Returns a bootstrap token your frontend passes to the embed iframe.
const { bootstrap_token, expires_in } = await vitra.session.create({
userId: "user-123",
parentOrigin: "https://app.translate.photo",
});vitra.credits.reserve / capture / release
These are the OUTBOUND direction — only relevant if your host app calls the Vitra credit endpoints directly rather than letting Image Creator call yours. Most integrations don't need these.
Error handling
All remote operations throw VitraError subclasses:
import { VitraCreditError, VitraRateLimitError } from "@vitra/image-creator-node";
try {
await vitra.credits.reserve({...});
} catch (err) {
if (err instanceof VitraCreditError) {
// 402 — user is out of credits. Show "add credits" flow. Do NOT retry.
return showBuyCreditsCTA();
}
if (err instanceof VitraRateLimitError) {
// 429 — back off for err.retryAfterSeconds
await sleep(err.retryAfterSeconds! * 1000);
return retry();
}
throw err; // other errors bubble up
}Error hierarchy (all instanceof VitraError):
| Class | HTTP | Meaning |
|-------|------|---------|
| VitraValidationError | 400 | Malformed request — fix and resubmit |
| VitraAuthError | 401 | Bad signature / expired token |
| VitraCreditError | 402 | Insufficient credits — terminal |
| VitraOriginError | 403 | Origin not in tenant allowlist |
| VitraRateLimitError | 429 | Back off; retryAfterSeconds on the error |
| VitraUnavailableError | 5xx / timeout | Transient — safe to retry with backoff |
| VitraConfigError | — | SDK misconfiguration (thrown before any network call) |
Local helpers (signWebhook, verifyWebhook) never throw (except
VitraConfigError on obviously broken input). They return Result-shaped
values you destructure.
Secret rotation
During the 24h rotation overlap, accept both the current AND previous shared secret when verifying:
const result = verifyWebhook({
header: req.headers.get("x-vitra-signature") ?? undefined,
body: raw,
sharedSecret: [
process.env.VITRA_IC_SECRET!, // current (new)
process.env.VITRA_IC_SECRET_PREVIOUS!, // previous (still valid for 24h)
],
});Drop VITRA_IC_SECRET_PREVIOUS from env after the overlap window.
Contract stability
Version 0.x is pre-1.0: the surface may change. From 1.0.0 onwards every
export listed in src/index.ts is semver-stable. Breaking changes require
a major version bump.
License
Apache-2.0 — see LICENSE.
