@ creativekit/ 服务器
OAuth2 认证的服务器端 CreativeKit SDK
⚠️ 只有节点.js - 从不暴露在浏览器中
快速启动
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 参考
创意
// 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);
// Or fill the draft from an https link instead of uploading (scope creative:ingest).
// CreativeKit downloads the file server-side, then continues as if commit() ran.
const status = await ck.creatives.ingest(id, {
sourceUrl: "https://assets.example.com/spot-30s.mp4",
// slot?: "video" - optional when the profile has one required slot
// idempotencyKey?: string
});
// status.status === "QUEUED"; status.source.status === "PENDING"
// On a failed download the creative ends FAILED; failure names step "source_fetch"
// and status.source.cause gives the reason (HTTP_STATUS, TOO_LARGE, ...).
// 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);
编辑只是草稿 .
reopenSlot编号 :replaceSlot需要创造力SUCCEEDED或者说FAILED财务报告和审定财务报表 未定稿. 后处理总是从原生上传出再运行,所以没有世代质量损失. 见 终到终流。 。 。 .
Auth (密钥生成)
在不暴露客户秘密的情况下, 生成用于客户端的短寿命符号 :
// 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
});
简介
// 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集团相关配置文件一起销售(例如: native 广告包 : 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,
});
卫生
// 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 }
用户
// 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();
调试
诊断认证问题的解决问题的方法:
// 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? }
终到终流
下方的每道流可复制-可复制. 它们涵盖整个创造性生命周期: 创建 /上传 / 承诺 / 监视 / 预览 / 定稿附加时 编辑 (重新开放/更换), 多相相/相机, (中文(简体) ), 预加载验证,以及 取消/重试/删除。 。 。 .
方法-终点参考
| SDK 方法 | REST 终点 |
|---|---|
ck.creatives.create(profile, opts) | POST /v1/creatives |
ck.creatives.commit(id, body?) | POST /v1/creatives/{id}/commit |
ck.creatives.ingest(id, opts) | POST /v1/creatives/{id}/ingest |
ck.creatives.upload(params) | 创建 + PUT + 承诺(服务器侧方便) |
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. 浏览器上传(建议)
浏览器通过预先签名的 URL 直接上传到存储; 您的后端
发布 URL 和 代理 生命周期 调用 。 用这个 @creativekit/client。 。 。 .
后端( Express) — 薄的代理 :
// 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(); });
浏览器侧( O)client.upload(...), (中文(简体) ), client.streamEvents(...)) 位于
@ creativekit/ 客户端 README。 。 。 .
2. 服务器端完整上传(文字、工人、队列)
ck.creatives.upload() 正在创建 ^ PUT 每个文件 ^ 承诺 ^ 等待,全部在
节点. 过 slotCounts 动态剖面图和 slotTransforms 用于农作物.
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. 木马(动力槽)
木马宣布 frame 模板插槽. slotCounts: { frame: N } 扩展它
输入 frame_1 … frame_N上传或预先签名
浏览器上传( 通过) slotCounts 改为 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 },
});
修改一个框架后将整个管道重新运行(所有框架从 他们的生上传。 参见流量 4.
4. 编辑已有的创意( 重新打开 替换)
交换一个空档/空档 草案 (未定稿)
重新创造它。 创造力必须是 SUCCEEDED 或者说 FAILED,另一个
插槽的生上传必须仍然在TTL草稿内.
选项A——完全服务器侧(一个呼叫):
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
});
选项B——浏览器驱动(已签名) :
// 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();
});
什么可以编辑? 只有插槽 目录 草稿(重新装入文件至 (a) 插槽) 配置/类型在创建时间被固定 。 一次
finalized,一个创造性是 不可改变, 克隆 (流程4b)。 公开GET /c/{id}/{filename}只服务于最终的创意, 所以活资产永远不会改变 在观看器下面.
4b. "Edit" 已定稿的创意(clone ~ 更改子集 ~ 已定稿)
最终的创意是不可改变的,所以编辑一个意味着产生一个 新设
创意. clone 使这个便宜:它把源头复制成新的草稿
(新身份证, AWAITING_UPLOAD和 服务器侧复制每个原始上传,所以请
没有改变的插槽不需要重新加载, 并且从原位置进行无损失的再处理
来源. 只更改您想要的槽, 然后承诺 + 完成 .
// 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.
删除或重排自動滑行( 不仅仅是互换文件内容), pass
slotPlan 复制时:
// 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返回 409 ——创建新的创意 换句话说.
5. 上载前验证(失效,无上载)
根据配置文件的规则检查已申报的元数据 在此之前 推动字节. 仅咨询——权威检查仍在实际字节执行时运行.
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. 取消、重试、删除、列表
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 事件代理服务器
代理实时处理事件到您的前端, 使用单个方法调用 :
// 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);
});
SDK的手柄:
- SSE 标题( Content- Type, 快取控制, 连接)
- * 终端状态检测(自动关闭SUCCEED、故障、CANCED)
- QQ 自动整流
- QQ 客户端断开处理
- QQ CORS 信头( 可选)
在CORS的支持和回调下:
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}`),
});
说明: SSE事件是最小信号(
id, (中文(简体) ),status, (中文(简体) ),progress) (中文(简体) ). 客户端应该打电话GET /creatives/:id关于终点站的详情.
补全 Webhooks
而不是投票或SSE,在创建时注册一个网页。 后端
邮递到您的 callbackUrl 当创意达到终端状态时
(单位:千美元)SUCCEEDED 编号 : FAILED 编号 : CANCELED),与您签名 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实值 所需经费 任何时候callbackUrl设置—— API 拒绝未签名的webhook注册(400).
接收网点 - 核实 X-CreativeKit-Signature 标题
(hex HMAC-SHA256 over the bird body) 与SDK帮助者:
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) 也导出
只想要布尔检查。 交付以指数回转
(~10个尝试)在5xx/网络错误上;2xx和4xx都算已交付,所以
将终端状态与 GET /v1/creatives/{id} 作为后退.
最终确定工作流程(草案-永久)
已处理的创意开始为"草稿",可以预览,但在草稿TTL过期后会自动被清理(默认:24h). 为了永久保持创造性:
// 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
为什么要最后决定?
- 成本控制: 仅用于确认的创意的存储费用
- 用户确认: 让用户在承诺前预览
- 《同上》: 安全呼叫多次
通过自动定稿跳过工作流程草稿 :
如果您不需要用户预览确认, 请自定义成功 :
const creative = await ck.creatives.create("video:web-1080p-h264", {
autoFinalize: true, // Automatically finalized when processing succeeds
});
表示端点示例 :
app.post("/creatives/:id/finalize", async (req, res) => {
await ck.creatives.finalize(req.params.id);
res.status(204).send();
});
预览 URL
生成已完成的创建的签名预览 URL :
// 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 });
表示端点示例 :
// 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);
});
处理失败的创意
当创作失败时, 请检查两者 results.validation (用于验证错误)和 failure (用于管道错误):
校验错误
详细校验错误在 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}`);
}
}
校验出错字段 :
| 外地 | 说明 |
|---|---|
field | 失败的财产(例如, aspectRatio, (中文(简体) ), duration(中文(简体) ) |
rule | 违反验证规则(例如:, allowedValues, (中文(简体) ), range(中文(简体) ) |
expected | 剖面图需要什么 |
actual | 在上传文件中发现了什么 |
code | 机器可读错误代码 |
message | 可读错误信息 |
profileHint | 概况限制的参考 |
管道错误
对于无法恢复的错误( 译码失败等) , 请检查 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}`);
}
快速端点示例
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);
});
完整文档
参见 SDK 主要文档 用于:
- 完整 API 参考
- 建筑格局
- 完整实例( Express, Next.js, NestJS)
- 安保最佳做法
- 解决问题
警卫
- 使用环境变量获取证书
- QQ 从不向浏览器曝光此 SDK
- 保留
clientSecret安全 - 在您的后端添加认证的中间软件
许可证
麻省理工学院