npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@creativekit/server

v1.0.42

Published

Server-side CreativeKit SDK with OAuth authentication

Readme

@creativekit/server

Server-side CreativeKit SDK with OAuth2 authentication

⚠️ Node.js only - Never expose to browsers


Quick Start

npm install @creativekit/server
import { CreativeKitServer } from "@creativekit/server";

const ck = new CreativeKitServer({
  apiBase: "https://api.adswag.nl",
  clientId: process.env.CREATIVEKIT_CLIENT_ID!,
  clientSecret: process.env.CREATIVEKIT_CLIENT_SECRET!,
});

// Create creative with presigned upload URL (AWS-style)
const creative = await ck.creatives.create("audio:podcast-aac");

// Return to frontend (client uploads directly to presigned URL)
// IMPORTANT: After upload, client must call commit to start processing
res.json({
  creativeId: creative.id,
  uploadUrl: creative.uploadSlots?.[0]?.url,
  uploadHeaders: creative.uploadSlots?.[0]?.headers,
});

// Commit endpoint - triggers processing
await ck.creatives.commit(creative.id);

API Reference

Creatives

// Create creative + get presigned URL (AWS-style)
const creative = await ck.creatives.create(profile, options);
// Options:
//   multipart?: boolean     - Use multipart upload for large files
//   fileSize?: number       - File size (required for multipart)
//   metadata?: object       - Custom metadata to attach
//   autoFinalize?: boolean  - Auto-finalize when processing succeeds (skip draft workflow)

// Commit after upload (starts processing)
await ck.creatives.commit(id);

// Get creative status and artifacts
const creative = await ck.creatives.get(id);
// Status values: "AWAITING_UPLOAD" | "QUEUED" | "PROCESSING" | "SUCCEEDED" | "FAILED" | "CANCELED"
// AWAITING_UPLOAD = created with directUpload, waiting for file upload + commit
// Progress is 0.0-1.0 (multiply by 100 for percentage)

// List creatives with filtering
const list = await ck.creatives.list({ status: "SUCCEEDED", limit: 20 });
// Options: status?, profile?, finalized?, limit?, pageToken?

// Finalize (mark as permanent, prevent auto-cleanup)
await ck.creatives.finalize(id);

// Generate preview URL for completed creative
const preview = await ck.creatives.preview(id);
// Returns: { url: string, expiresAt: number }

// Management
await ck.creatives.delete(id);
await ck.creatives.cancel(id);
await ck.creatives.retry(id);

// Edit a draft: reopen one slot → (client uploads) → replace to reprocess
const { uploadSlots } = await ck.creatives.reopenSlot(id, "frame_2");
await ck.creatives.replaceSlot({ id, slot: "frame_2" }); // file already re-uploaded
// …or do the whole thing server-side in one call (reopen + upload + reprocess):
await ck.creatives.replaceSlot({ id, slot: "frame_2", file: newImage });

// Edit a FINALIZED creative: clone into a fresh draft, change a subset of
// slots, then commit + finalize (the original is never modified).
const draft = await ck.creatives.clone(finalizedId); // copies all raw uploads
// draft.uploadSlots covers every slot; PUT new files only for the changed ones
await ck.creatives.commit(draft.id);
await ck.creatives.finalize(draft.id);

// Server-side upload (Node.js scripts/workers); supports slotCounts + slotTransforms
const result = await ck.creatives.upload({ profile, files });

// SSE proxy to frontend (recommended)
await ck.creatives.getEventsStream(id, res);

Editing is draft-only. reopenSlot/replaceSlot require the creative to be SUCCEEDED or FAILED and not finalized. Reprocessing always re-runs from the original raw uploads, so there is no generational quality loss. See End-to-End Flows.

Auth (Client Token Generation)

Generate short-lived tokens for client-side use without exposing your client secret:

// Generate a scoped access token
const tokenData = await ck.auth.generateToken("creative:create creative:read creative:commit");

// Return to frontend
res.json({
  token: tokenData.accessToken,
  expiresIn: tokenData.expiresIn, // seconds
});

Profiles

// List available processing profiles
const profiles = await ck.profiles.list();
// Returns: Profile[] with id, type, description, etc.

// Pre-validate declared file metadata against a profile WITHOUT uploading.
// Advisory pre-flight check — the authoritative validation still runs on the
// real bytes after upload. Returns the same { errors, warnings } shapes the
// processing pipeline emits, namespaced per slot (e.g. "standard.aspectRatio").
const result = await ck.profiles.validateMetadata("native:image", {
  slots: {
    standard: {
      filename: "banner.png",
      mimeType: "image/png",
      sizeBytes: 1_048_576,
      width: 1200,
      height: 628,
    },
  },
});
// Returns: { passed: boolean, errors: ValidationError[], warnings?: ValidationWarning[] }

// For dynamic-slot profiles (e.g. native:carousel), pass slotCounts so per-frame
// slots (frame_1..frame_N) and slot-group rules resolve:
await ck.profiles.validateMetadata("native:carousel", {
  slotCounts: { frame: 4 },
  slots: { frame_1: { width: 1080, height: 1080, sizeBytes: 500_000 } },
});

Bundles

Bundles group related profiles sold together (e.g. the native ad pack: standard + large).

// List all bundles. Pass { expandProfiles: true } to inline each member's
// full profile spec (upload slots + validation rules) in one request.
const bundles = await ck.profiles.listBundles({ expandProfiles: true });

// Get a single bundle by id (throws ApiError with status 404 if missing)
const bundle = await ck.profiles.getBundle("native:standard", {
  expandProfiles: true,
});

Health

// Comprehensive health check (API + credentials)
const health = await ck.health.check();
// Returns: { isHealthy, apiReachable, credentialsValid, details }

// Basic connectivity test
const reachable = await ck.health.ping();
// Returns: boolean

// Validate OAuth credentials
const validation = await ck.health.validateCredentials();
// Returns: { valid: boolean, error?: string }

Utils

// Check if client has valid credentials configured
const authenticated = ck.utils.isAuthenticated();

// Get current API configuration
const config = ck.utils.getConfig();
// Returns: { apiBase, hasCredentials }

// Get last known connection status
const status = ck.utils.getConnectionStatus();

Debug

Troubleshooting methods for diagnosing authentication issues:

// Validate credentials with detailed error info
const result = await ck.debug.validateCredentials();
// Returns: { valid, error?, details? }

// Test API connection with diagnostics
const conn = await ck.debug.testConnection();
// Returns: { apiReachable, credentialsValid, responseTime?, error?, details? }

// Get current token information
const tokenInfo = await ck.debug.getTokenInfo();
// Returns: { hasCredentials, tokenCached, tokenExpiry?, error? }

End-to-End Flows

Every flow below is copy-pasteable. They cover the full creative lifecycle: create → upload → commit → watch → preview → finalize, plus editing (reopen/replace), multi-slot/carousel, pre-upload validation, and cancel/retry/delete.

Method → endpoint reference

| SDK method | REST endpoint | | -------------------------------------------- | ---------------------------------------------- | | ck.creatives.create(profile, opts) | POST /v1/creatives | | ck.creatives.commit(id, body?) | POST /v1/creatives/{id}/commit | | ck.creatives.upload(params) | create + PUT + commit (server-side convenience) | | ck.creatives.get(id) | GET /v1/creatives/{id} | | ck.creatives.list(opts) | GET /v1/creatives | | ck.creatives.cancel(id) / retry(id) | PATCH /v1/creatives/{id} | | ck.creatives.delete(id) | DELETE /v1/creatives/{id} | | ck.creatives.reopenSlot(id, slot) | POST /v1/creatives/{id}/slots/{slot}/reopen | | ck.creatives.replaceSlot(params) | POST /v1/creatives/{id}/slots/{slot}/replace | | ck.creatives.preview(id) | POST /v1/creatives/{id}/preview | | ck.creatives.finalize(id) | POST /v1/creatives/{id}/finalize | | ck.creatives.clone(id) | POST /v1/creatives/{id}/clone | | ck.creatives.getEventsStream(id, res) | GET /v1/creatives/{id}/events (SSE) | | ck.profiles.list(opts?) | GET /v1/profiles | | ck.profiles.validateMetadata(id, body) | POST /v1/profiles/{id}/validate | | ck.profiles.listBundles(opts?) | GET /v1/bundles | | ck.profiles.getBundle(id, opts?) | GET /v1/bundles/{id} | | ck.auth.generateToken(scopes) | POST /oauth/token |

1. Browser upload (recommended)

The browser uploads directly to storage via presigned URLs; your backend only issues URLs and proxies lifecycle calls. Use this with @creativekit/client.

Backend (Express) — thin proxy:

// Create + return presigned upload slot(s)
app.post("/api/creatives", async (req, res) => {
  const { profile, slotCounts } = req.body;
  const creative = await ck.creatives.create(profile, { slotCounts });
  res.json({ creativeId: creative.id, uploadSlots: creative.uploadSlots });
});

// Commit to start processing (optionally pass slotTransforms for crops)
app.post("/api/creatives/:id/commit", async (req, res) => {
  await ck.creatives.commit(req.params.id, { slotTransforms: req.body?.slotTransforms });
  res.status(204).send();
});

// Status + live events + finalize
app.get("/api/creatives/:id", async (req, res) => res.json(await ck.creatives.get(req.params.id)));
app.get("/api/creatives/:id/events", (req, res) => ck.creatives.getEventsStream(req.params.id, res, { corsOrigin: req.headers.origin }));
app.post("/api/creatives/:id/finalize", async (req, res) => { await ck.creatives.finalize(req.params.id); res.status(204).send(); });

The browser side (client.upload(...), client.streamEvents(...)) is in the @creativekit/client README.

2. Fully server-side upload (scripts, workers, queues)

ck.creatives.upload() does create → PUT each file → commit → wait, all in Node. Pass slotCounts for dynamic profiles and slotTransforms for crops.

import { readFile } from "node:fs/promises";

// Single-file profile
const audio = await ck.creatives.upload({
  profile: "audio:podcast-aac",
  files: { audio: new File([await readFile("ep.mp3")], "ep.mp3", { type: "audio/mpeg" }) },
  // `progress` (0-100) is the canonical progress field across CreativeKit
  // SDKs (the client SDK's `percent` is a deprecated alias of it).
  onProgress: (p) => console.log(`${p.stage} ${p.progress}%`),
});

// Native ad pack (multi-slot): "standard" + "large" are sold as one bundle.
// Each slot is a separate file; optionally crop each to its target.
const native = await ck.creatives.upload({
  profile: "native:image",
  files: { standard: standardFile, large: largeFile },
  slotTransforms: {
    // crop rect is in SOURCE pixels; target is the output resolution after crop+scale
    standard: { aspectRatio: "1.91:1", cropX: 0, cropY: 0, cropW: 1200, cropH: 628, targetWidth: 1200, targetHeight: 628 },
  },
});

if (native.status === "SUCCEEDED") console.log(native.artifacts);

3. Carousel (dynamic slots)

Carousels declare a frame template slot. slotCounts: { frame: N } expands it into frame_1 … frame_N. Works the same for server-side upload or presigned browser upload (pass slotCounts to create).

const carousel = await ck.creatives.upload({
  profile: "native:carousel",
  slotCounts: { frame: 4 },
  files: { frame_1: f1, frame_2: f2, frame_3: f3, frame_4: f4 },
});

Changing one frame later re-runs the whole pipeline (all frames re-encode from their raw uploads). See flow 4.

4. Edit an existing creative (reopen → replace)

Swap a single slot/frame of a draft (non-finalized) creative without recreating it. The creative must be SUCCEEDED or FAILED, and the other slots' raw uploads must still be within the draft TTL.

Option A — fully server-side (one call):

const updated = await ck.creatives.replaceSlot({
  id: creativeId,
  slot: "frame_2",
  file: newFrameFile,           // reopen + upload + reprocess + wait
  slotTransform: { aspectRatio: "1:1", cropX: 0, cropY: 0, cropW: 1080, cropH: 1080, targetWidth: 1080, targetHeight: 1080 }, // optional
});

Option B — browser-driven (presigned):

// 1) Backend reopens the slot and returns fresh presigned URLs
app.post("/api/creatives/:id/slots/:slot/reopen", async (req, res) => {
  const { uploadSlots } = await ck.creatives.reopenSlot(req.params.id, req.params.slot);
  res.json({ uploadSlots });
});

// 2) Browser PUTs the new file to that URL (client.replaceSlot handles this)

// 3) Backend triggers reprocessing (no file arg → just reprocess)
app.post("/api/creatives/:id/slots/:slot/replace", async (req, res) => {
  await ck.creatives.replaceSlot({
    id: req.params.id,
    slot: req.params.slot,
    slotTransform: req.body?.slotTransform, // optional crop from the browser
  });
  res.status(204).send();
});

What can be edited? Only slot contents of a draft (re-upload a file to a slot). Profile/type are fixed at create time. Once finalized, a creative is immutable — to change it, clone it (flow 4b). Public GET /c/{id}/{filename} only serves finalized creatives, so live assets never change underneath a viewer.

4b. "Edit" a finalized creative (clone → change subset → finalize)

Finalized creatives are immutable, so editing one means producing a new creative. clone makes that cheap: it duplicates the source into a fresh draft (new ID, AWAITING_UPLOAD) and server-side copies every raw upload, so the unchanged slots need no re-upload and reprocess losslessly from the original source. Change only the slots you want, then commit + finalize.

// 1) Clone the finalized creative → new draft seeded with all original frames
app.post("/api/creatives/:id/clone", async (req, res) => {
  const draft = await ck.creatives.clone(req.params.id);
  // draft.uploadSlots covers EVERY slot (e.g. frame_1..frame_8)
  res.status(201).json({ id: draft.id, uploadSlots: draft.uploadSlots });
});

// 2) Browser PUTs new files ONLY for the changed slots (e.g. 4 of 8 frames)
//    to their presigned URLs. The other slots keep the copied raw uploads.

// 3) Commit reprocesses all slots from raw (one clean pass), then finalize.
await ck.creatives.commit(draftId);
await ck.creatives.finalize(draftId);
// Result: a brand-new finalized creative (new ID/URL). The original is untouched;
// the platform should swap its reference to the new creative.

To remove or reorder carousel slides (not just swap file contents), pass slotPlan on clone:

// 5-frame source → 3-frame draft (drops frame_2 and frame_4)
const draft = await ck.creatives.clone(finalizedId, {
  slotPlan: { frame: ["frame_1", "frame_3", "frame_5"] },
});
// draft.uploadSlots = frame_1..frame_3, seeded from source frame_1, frame_3, frame_5

Clone relies on the source still having its raw uploads (finalized creatives retain them). If they are gone, clone returns 409 — create a fresh creative instead.

5. Pre-upload validation (fail fast, no upload)

Check declared metadata against a profile's rules before pushing bytes. Advisory only — the authoritative check still runs on the real bytes at commit.

const check = await ck.profiles.validateMetadata("native:image", {
  slots: { standard: { mimeType: "image/png", sizeBytes: 1_048_576, width: 1200, height: 628 } },
});

if (!check.passed) {
  // surface check.errors to the user and skip the upload
  return res.status(422).json({ errors: check.errors, warnings: check.warnings });
}
// else proceed with create/upload

6. Cancel, retry, delete, list

await ck.creatives.cancel(id);        // stop in-flight processing
await ck.creatives.retry(id);         // retry a FAILED creative
await ck.creatives.delete(id);        // remove a creative + its artifacts

// Paginate finalized creatives for a profile
let pageToken: string | undefined;
do {
  const page = await ck.creatives.list({ profile: "native:image", finalized: true, limit: 50, pageToken });
  page.items.forEach((c) => console.log(c.id, c.status));
  pageToken = page.nextPageToken;
} while (pageToken);

SSE Events Proxy

Proxy real-time processing events to your frontend with a single method call:

// NestJS
@Get(':id/events')
async streamEvents(@Param('id') id: string, @Res() res: Response) {
  await ck.creatives.getEventsStream(id, res);
}

// Express
app.get('/creatives/:id/events', async (req, res) => {
  await ck.creatives.getEventsStream(req.params.id, res);
});

The SDK handles:

  • ✅ SSE headers (Content-Type, Cache-Control, Connection)
  • ✅ Terminal state detection (auto-closes on SUCCEEDED, FAILED, CANCELED)
  • ✅ Automatic stream cleanup
  • ✅ Client disconnect handling
  • ✅ CORS headers (optional)

With CORS support and callbacks:

await ck.creatives.getEventsStream(id, res, {
  corsOrigin: req.headers.origin,
  onComplete: (status) => console.log(`Creative ${id}: ${status}`),
  onError: (error) => console.error(`Stream error: ${error.message}`),
});

Note: SSE events are minimal signals (id, status, progress). Clients should call GET /creatives/:id for full details on terminal states.


Completion Webhooks

Instead of polling or SSE, register a webhook at create time. The backend POSTs to your callbackUrl when the creative reaches a terminal status (SUCCEEDED / FAILED / CANCELED), signed with your callbackSecret.

const creative = await ck.creatives.create("video:web-1080p-h264", {
  callbackUrl: "https://your-backend.example.com/webhooks/creativekit",
  callbackSecret: process.env.CREATIVEKIT_WEBHOOK_SECRET!, // required with callbackUrl
});

callbackSecret is required whenever callbackUrl is set — the API rejects unsigned webhook registrations (400).

Receiving webhooks — verify the X-CreativeKit-Signature header (hex HMAC-SHA256 over the raw body) with the SDK helpers:

import express from "express";
import { parseWebhookEvent, WEBHOOK_SIGNATURE_HEADER } from "@creativekit/server";

// Use a raw body parser: the signature covers the exact bytes received.
app.post(
  "/webhooks/creativekit",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    let event;
    try {
      event = parseWebhookEvent(req.body, {
        secret: process.env.CREATIVEKIT_WEBHOOK_SECRET!,
        signature: req.header(WEBHOOK_SIGNATURE_HEADER),
      });
    } catch {
      return res.sendStatus(401);
    }

    // Delivery is at-least-once — dedupe on event.eventId.
    // The payload is minimal ({ eventId, id, status, error }); on SUCCEEDED,
    // fetch the creative for artifacts and CDN URLs:
    if (event.status === "SUCCEEDED") {
      const creative = await ck.creatives.get(event.id);
      // ...store creative.artifacts...
    }

    res.sendStatus(204);
  }
);

verifyWebhookSignature(secret, rawBody, signature) is also exported if you only want the boolean check. Deliveries are retried with exponential backoff (~10 attempts) on 5xx/network errors; 2xx and 4xx both count as delivered, so reconcile terminal states with GET /v1/creatives/{id} as a fallback.


Finalize Workflow (Draft → Permanent)

Processed creatives start as "drafts" that can be previewed but will be automatically cleaned up after the draft TTL expires (default: 24h). To keep a creative permanently:

// After user reviews the preview and confirms
await ck.creatives.finalize(creativeId);

// Check finalization status
const creative = await ck.creatives.get(creativeId);
console.log(creative.finalized); // true
console.log(creative.finalizedAt); // timestamp in ms

Why finalize?

  • Cost control: Storage costs only for confirmed creatives
  • User confirmation: Let users preview before committing
  • Idempotent: Safe to call multiple times

Skip draft workflow with autoFinalize:

If you don't need user preview confirmation, auto-finalize on success:

const creative = await ck.creatives.create("video:web-1080p-h264", {
  autoFinalize: true, // Automatically finalized when processing succeeds
});

Express endpoint example:

app.post("/creatives/:id/finalize", async (req, res) => {
  await ck.creatives.finalize(req.params.id);
  res.status(204).send();
});

Preview URLs

Generate signed preview URLs for completed creatives:

// Get a signed preview URL (expires after a period)
const { url, expiresAt } = await ck.creatives.preview(creativeId);

// Return to frontend for display
res.json({ previewUrl: url, expiresAt });

Express endpoint example:

// Note: The SDK uses POST internally to generate the preview URL
app.post("/creatives/:id/preview", async (req, res) => {
  const preview = await ck.creatives.preview(req.params.id);
  res.json(preview);
});

Handling Failed Creatives

When a creative fails, check both results.validation (for validation errors) and failure (for pipeline errors):

Validation Errors

Detailed validation errors are in results.validation.errors:

const creative = await ck.creatives.get(id);

if (creative.status === "FAILED" && creative.results?.validation) {
  console.log("Validation failed:");
  for (const error of creative.results.validation.errors) {
    console.log(`  ${error.field}: ${error.message}`);
    console.log(`    Expected: ${error.expected}, Got: ${error.actual}`);
    console.log(`    Code: ${error.code}`);
  }
}

Validation Error Fields:

| Field | Description | | ----- | ----------- | | field | Property that failed (e.g., aspectRatio, duration) | | rule | Validation rule violated (e.g., allowedValues, range) | | expected | What the profile requires | | actual | What was found in the uploaded file | | code | Machine-readable error code | | message | Human-readable error message | | profileHint | Reference to the profile constraint |

Pipeline Errors

For unrecoverable errors (transcode failures, etc.), check failure:

if (creative.status === "FAILED" && creative.failure) {
  console.log(`Pipeline failed at step: ${creative.failure.step}`);
  console.log(`Error: ${creative.failure.message}`);
  console.log(`Retriable: ${creative.failure.retriable}`);
}

Express Endpoint Example

app.get("/api/creatives/:id", async (req, res) => {
  const creative = await ck.creatives.get(req.params.id);

  // Return validation errors in a client-friendly format
  if (creative.status === "FAILED") {
    const validationErrors = creative.results?.validation?.errors || [];
    const pipelineError = creative.failure;

    return res.json({
      ...creative,
      // Flatten for easier client consumption
      errorSummary: validationErrors.length
        ? validationErrors.map((e) => e.message).join("; ")
        : pipelineError?.message || "Unknown error",
    });
  }

  res.json(creative);
});

Full Documentation

See the main SDK documentation for:

  • Complete API reference
  • Architecture patterns
  • Full examples (Express, Next.js, NestJS)
  • Security best practices
  • Troubleshooting

Security

  • ✅ Use environment variables for credentials
  • ✅ Never expose this SDK to browsers
  • ✅ Keep clientSecret secure
  • ✅ Add auth middleware to your backend

License

MIT