API Reference - 2DAICommunity/2dai-cloud-sdk GitHub Wiki

API Reference

new Client(options)

interface ClientOptions {
  apiKey: string;               // required β€” '2dai_sk_…'
  baseUrl?: string;             // default 'https://dapp.2dai.io:444'
  fetch?: typeof fetch;
  timeoutMs?: number;           // default 60000
  maxRetries?: number;          // default 2
  integration?: 'sdk' | 'mcp';  // default 'sdk'
  integrationVersion?: string;  // semver, e.g. '1.4.0'
}

integration is a closed set, not a free-form User-Agent: the server derives each creation's source from the client token, and source is what puts a row in the dashboard's SDK / MCP collections and picks its origin pill. Only the official 2DAI MCP server sends 'mcp' β€” your own app, a script or a backend job leaves this alone and lands as sdk. integrationVersion rides along for diagnostics and must look like 1.2.3 (a prerelease or build suffix is fine); a malformed value is replaced by 0.0.0 rather than omitted β€” the token has to match name/x.y.z for the server to recognise the integration at all, so an honest placeholder beats losing the attribution.

  • client.baseUrl: string
  • client.me(signal?): Promise<Account>
  • client.download(ref, opts?: DownloadOptions) β€” shortcut for client.cdn.download

client.generate

generate.image(params: ImageParams, opts?: SubmitOptions): Promise<GenerationResult>       // { wait:true } default
generate.image(params: ImageParams, opts: { wait:false } & SubmitOptions): Promise<QueueTicket>
generate.imageWithRefs(params: RefParams, opts?): Promise<GenerationResult | QueueTicket>
generate.wallpaper(params: WallpaperParams, opts?): Promise<GenerationResult | QueueTicket>
generate.artisticStyle(params: ArtisticStyleParams, opts?): Promise<GenerationResult | QueueTicket>   // curated artistic style
generate.video(params: VideoParams, opts?): Promise<GenerationResult | QueueTicket>
generate.similar(ref: CreationRef, opts?): Promise<GenerationResult | QueueTicket>         // re-run a stored creation

SubmitOptions: { wait? (default true), intervalMs? (default 2000), timeoutMs? (default 300000), signal? } β€” same defaults for WaitOptions.

interface ImageParams {
  prompt: string;
  imageModel?: 'default' | 'next';   // 'next' = Gen8 Preview images: ≀6 references, legible text β€” opens at Holder (me().canUseImageNext)
  refCreationIds?: string[];     // ≀3 soft-conditioning refs Β· ≀6 on imageModel 'next' (each adds a surcharge)
  style?; quality?; aspectRatio?; width?; height?; negativePrompt?; enhanced?; allowNSFW?; clientToken?;
  // negativePrompt: optional β€” applied by Gen8 Preview images, the Gen7 image engine renders without it
}
interface RefParams {
  tool: 'character-ref' | 'style-transfer' | 'smart-edit';   // 'face-ref' merged into 'character-ref' (3.0.0)
  refCreationIds: string[];      // ≀6 identity refs (character; face photos lock the face) Β· ≀6 style sources Β·
                                 // smart-edit: refs[0] = the image to EDIT, ≀6 total
  prompt?; quality?; aspectRatio?; width?; height?; allowNSFW?;
  extractionDirective?;          // style-transfer only, default 'style'
  clientToken?;
}
interface WallpaperParams {
  inputCreationId: string;       // the creation to expand/resize
  dimension: string;             // target wallpaper dimension β€” drives pricing (fail-closed);
                                 // quality is forced to Ultra server-side
  refCreationIds?: string[];     // ≀3 extra soft-conditioning refs
  prompt?; allowNSFW?; clientToken?;
}
interface VideoParams {
  prompt: string; inputCreationId: string;
  videoModel?: 'default' | 'next';   // 'next' = Gen8 Preview video: clips WITH sound
  duration?;                     // Video: 5, 6 or 7 (7 tier-gated) Β· Gen8 Preview video: one of me().videoDurationsNext
  refCreationIds?: string[];     // Gen8 Preview video only: ≀6 references after the first frame (each adds a surcharge)
  aspectRatio?;                  // Gen8 Preview video only: 'auto' (the first frame's ratio) or a ratio
  audioCreationId?: string;      // Gen8 Preview video only: an uploaded MP3 (uploads.audio) as the audio reference β€” counts as one more reference
  audioUse?: 'music' | 'voice';  // soundtrack / sound design (default) Β· a voice sample the speaking character follows
  quality?; style?; enhanced?; frameInterpolation?; allowNSFW?; clientToken?;   // enhanced: TIXI on style 'raw' (other styles always run TIXI) Β· frameInterpolation: Video only
}

Reference caps are strict β€” exceeding a tool's cap rejects with a 400 TOO_MANY_REFS (carrying max and got) before anything is charged.

client.queue

queue.get(queueId: string, signal?): Promise<QueueState>
queue.waitFor(queueId: string, opts?: WaitOptions): Promise<QueueState>  // resolves on any terminal status
queue.cancel(queueId: string, signal?): Promise<{ queueId, status: 'cancelled' }>

cancel only works while the item is still pending β€” the charge is refunded and the queue slot released. Once a worker picked it up the server answers 409 NOT_PENDING (current status in details.status).

client.creations

creations.list(opts?: ListOptions): Promise<CreationPage>
creations.random(opts?: RandomOptions): Promise<Creation | null>   // ONE uniform sample, same filters, no paging
creations.feed(opts?: FeedOptions): Promise<FeedPage>              // the cross-user public feed
creations.get(ref: CreationRef, signal?): Promise<Creation>
creations.clone(ref, signal?): Promise<Creation>                   // byte-identical duplicate (images only)
creations.move(ref, target: string | null, signal?): Promise<{ creationId, folderId }>  // 'root'|null = detach
creations.trash(ref, signal?): Promise<{ creationId, trashed }>
creations.restore(ref, signal?): Promise<{ creationId, trashed }>
creations.delete(ref, signal?): Promise<{ creationId, deleted }>          // must be trashed first
creations.publish(ref, signal?): Promise<{ creationId, isPublicShared }>
creations.unpublish(ref, signal?): Promise<{ creationId, isPublicShared }>

creations.like(ref, liked: boolean, signal?): Promise<{ liked, likeCount, changed }>  // idempotent SET
creations.flagNsfw(ref, signal?): Promise<{ nsfwFlagged: true, nsfwManual, nsfwRate }>
creations.explainNsfw(ref, signal?): Promise<{ explanation, cached, nsfwRate?, nsfwThreshold }>
creations.scoreFaceRef(ref, signal?): Promise<{ score, scoredAt?, cached }>
creations.scoreCharacterRef(ref, signal?): Promise<{ score, scoredAt?, cached }>
creations.reorder(creationIds: string[], signal?): Promise<{ reordered }>             // ≀1000 ids
creations.batch(action: 'trash'|'untrash'|'delete', ids: string[], signal?)
  : Promise<{ action, requested, processed, skipped }>                                // ≀1000 ids
interface ListOptions extends CreationFilterOptions {
  limit?: number;        // 1..100, default 20
  page?: number;         // 1-based offset paging
  beforeDate?: string;   // cursor β€” prefer it for deep walks
}
interface CreationFilterOptions {  // shared by list + random
  // modes β€” mutually exclusive:
  folderId?: string;               // 'root' = ungrouped root
  trashed?: boolean;               // the trash (default listing only)
  activity?: CreationActivity;     // studio fixed collections
  smart?: string;                  // 'faces'|'videos'|'crop'|'alpha'|'favorites'
  sharedFolderId?: string;         // a folder shared WITH you
  groupId?: string;                // a folder group β€” every folder filed in it (your own groups)
  // cross-cutting filters β€” compose with any mode:
  search?: string;                 // whole-word AND; trailing * = prefix; ≀128 chars / 8 tokens
  sort?: CreationSort;
  ratioFilter?: RatioBucket | string;  // 'square'|'landscape'|'portrait', or a comma set
  from?: string | number;          // ISO or epoch ms
  to?: string | number;
  hideFiled?: boolean;             // history/uploaded/likes/sdk/mcp lenses only
  usedRef?: CreationRef;           // creations BUILT FROM this one (refs + lineage)
  signal?: AbortSignal;
}
type CreationActivity = 'all'|'history'|'uploaded'|'likes'|'public'|'trash'|'sdk'|'mcp'|'portfolio'
type CreationSort = 'newest'|'oldest'|'updated'|'size-desc'|'size-asc'|'type-asc'|'type-desc'
type RandomOptions = CreationFilterOptions   // no paging β€” samples the whole filtered collection
interface FeedOptions { limit?; page?; includeNsfw?; signal? }   // limit 1..48 default 24; page 1-based ≀1000

Combining two modes (or trashed with anything but the default listing) rejects with 400 CONFLICTING_FILTERS. random on the trash rejects with 400 RANDOM_NOT_SUPPORTED; passing random to list is rejected client-side with the same code β€” list always returns a page.

flagNsfw is owner-only (safe β†’ NSFW escalation). explainNsfw and the two ref scores are cached on the row: the first call burns a vision-LLM pass (hence the generate scope), repeats are free.

client.folders

folders.list(arg?: AbortSignal | FolderListOptions): Promise<FolderPage>
folders.create(input: { title, description?, groupId? }, signal?): Promise<Folder>   // max 500 folders
folders.update(folderId, patch: FolderPatch, signal?): Promise<Folder>
folders.delete(folderId, opts?: { trashContents?, signal? }): Promise<{ folderId, deleted }>

folders.groups.list(signal?): Promise<FolderGroup[]>                    // sidebar (index) order
folders.groups.create(title: string, signal?): Promise<FolderGroup>     // max 100 groups
folders.groups.rename(groupId, title, signal?): Promise<FolderGroup>
folders.groups.remove(groupId, signal?): Promise<{ groupId, deleted }>  // detaches member folders only

interface FolderListOptions { limit?; beforeDate?; sort?: FolderSort; signal? }
type FolderSort = 'index' | 'date-desc' | 'date-asc' | 'name-asc' | 'name-desc' | 'update-desc'
interface FolderPatch {
  title?: string;
  description?: string;
  groupId?: string | null;           // string = move into a group, null = detach
  posterCreationId?: string | null;  // string = pin as poster, null = clear
  isFavorite?: boolean;              // star/unstar in the sidebar
}
interface FolderGroup { groupId; title; sortIndex?; collapsed?; createdAt?; updatedAt? }

list returns a page (newest first by default, limit 1..100 default 20) β€” paginate with nextBeforeDate. The cursor is only emitted for the default and date-desc orderings: beforeDate filters on the creation date, so any other sort is honoured but comes back without nextBeforeDate β€” raise limit instead. The bare-AbortSignal call form (folders.list(signal)) still works. Hitting a cap answers 409 FOLDER_LIMIT_REACHED / GROUP_LIMIT_REACHED.

client.artisticStyles

artisticStyles.list(signal?): Promise<ArtisticStyle[]>   // { artisticStyleId, title, blurb?, thumbnailCdnId?, thumbnailCdnExt? }

Read scope. The curated styles available to generate.artisticStyle; fetch a vignette's bytes with cdn.fetch(style.thumbnailCdnId).

client.uploads

uploads.image(input: UploadInput): Promise<Creation>
uploads.audio(input: UploadInput): Promise<Creation>   // an MP3 (5 min max, Holder+) β†’ the audio reference of a Gen8 Preview video clip
uploads.checkDuplicate(md5: string, folderId?: string, signal?): Promise<DuplicateCheck>

interface UploadInput {
  path?; data?: Uint8Array|ArrayBuffer|Blob; base64?;   // exactly one source
  filename?; contentType?;
  croppedFromCreationId?;   // record as a CROP of a creation you own   ─┐ mutually
  erasedFromCreationId?;    // record as an ERASE edit                  β”€β”˜ exclusive
  targetFolderId?;          // file the upload into this folder (writable only)
  signal?;
}
interface DuplicateCheck { duplicate: boolean; existingCreationId: string | null; folderId: string | null }

checkDuplicate is an advisory pre-flight (scope read): the upload itself still re-checks, so treat a false as a hint, not a lock. A targetFolderId you can't write to answers 403 INVALID_FOLDER and nothing is kept.

client.stats

stats.overview(signal?): Promise<StatsOverview>       // storage, counters, streak, smart counts, per-tool recents
stats.generations(opts?: { days?: 30 | 90; signal? }): Promise<StatsGenerations>
stats.top(signal?): Promise<StatsTop>                 // most-used refs (+ suggested config), styles, keywords

All three need only the read scope. overview and top are cache-backed β€” their cache: { computedAt, ageSeconds, stale } says how fresh the numbers are, and stale: true means a background refresh was kicked (re-fetch shortly). generations is a live aggregate memoized ~60 s per window; any days other than 30 or 90 is a 400. See Stats & Wallet.

client.finance

finance.wallet(signal?): Promise<FinanceWallet>              // tokens, creditUsd, tier, pending flags
finance.tier(signal?): Promise<AccountTier>                  // effective tier + the 4 legs it's the max of
finance.lock(signal?): Promise<WalletLockStatus>             // 'none' | 'active' | 'expiring-soon'
finance.transactions(opts?: { limit?, page?, signal? }): Promise<TransactionPage>   // page 0-based; limit 5..50
finance.balanceHistory(opts?: { limit?, signal? }): Promise<BalanceHistory>         // limit = DAYS 7..365, default 90
finance.creditHistory(opts?: { limit?, signal? }): Promise<CreditHistory>           // limit = DAYS 7..365, default 90
finance.creditSources(opts?: { days?, signal? }): Promise<CreditSources>            // days 1..365, default 30
finance.tokenPrice(signal?): Promise<TokenPrice>             // scope `read`; 503 PRICE_UNAVAILABLE when no cache
finance.tiers(signal?): Promise<TierInfo[]>                  // scope `read`; the public tier catalogue

Everything here is read-only and (except tokenPrice / tiers) requires a key minted with the opt-in finance scope β€” money mutations don't exist on the API-key surface at all. See Stats & Wallet.

client.cdn

cdn.url(ref: CdnRef, transform?: CdnTransform): string
cdn.fetch(ref: CdnRef, arg?: CdnFetchArg): Promise<CdnAsset>   // { bytes, contentType, ext }
cdn.download(ref, opts?: DownloadOptions): Promise<string | CdnAsset>
cdn.toBlob(ref, arg?: CdnFetchArg): Promise<Blob>
cdn.toDataUrl(ref, arg?: CdnFetchArg): Promise<string>

interface CdnTransform {
  width?: number;      // β†’ ?w β€” target width in px
  height?: number;     // β†’ ?h β€” target height in px
  maxSide?: number;    // β†’ ?s β€” longest edge in px, aspect ratio preserved
}
interface CdnFetchOptions extends CdnTransform { signal?: AbortSignal }
interface DownloadOptions extends CdnTransform { savePath?: string; signal?: AbortSignal }
type CdnFetchArg = AbortSignal | CdnFetchOptions

The resize happens on the CDN, before the bytes are sent β€” nothing is decoded client-side, so a preview costs a fraction of the transfer. maxSide on its own scales the longest edge and keeps the aspect ratio; width / height target those axes specifically and may crop. Non-finite or non-positive values throw ApiError('INVALID_TRANSFORM') locally, before any request goes out; valid ones are rounded to whole pixels. fetch / toBlob / toDataUrl still accept a bare AbortSignal as their second argument, so pre-transform code keeps working; pass { …transform, signal } to combine the two. download composes the same way β€” savePath plus a transform writes a resized file.

Core types

interface Account {
  userId; username?; creditUsd; tier; canDisableWatermark?; key: KeyContext;
  quality?: { ultraImage; ultraVideo; ultimateImage; ultimateVideo };   // tier-gated caps (SDK β‰₯ 2.5.4)
  qualities?: { image: QualityPreset[]; video: QualityPreset[]; videoNext?: QualityPreset[]; imageNext?: QualityPreset[] };   // every preset with `allowed` + `recommended`; imageNext = the Gen8 Preview images presets
  videoDurations?: { value; label; locked; recommended? }[]; defaultVideoDuration?  // 5 s
  canUseVideo?;                                                          // video (and Gen8 Preview video) open to the tier
  canUseImageNext?;                                                      // Gen8 Preview images (imageModel 'next') open to the tier and the key (SDK β‰₯ 2.8.0)
  videoDurationsNext?: { value; label; locked; recommended? }[]; defaultVideoDurationNext?   // Gen8 Preview video, when open
  modelChannels?: { video: { default; next }; image: { default; next } }  // engine id behind each channel, null = off
}
interface QualityPreset { id; name; description; allowed; recommended }
interface KeyContext { keyId; label; scopes: Scope[]; spendLimitUsd: number|null; spentUsd; modelChannels?: ('default'|'next')[] }   // modelChannels: the channels this key's policy may submit to (absent = every channel)
type Scope = 'read' | 'generate' | 'manage' | 'publish' | 'finance'

interface QueueTicket { queueId; status: QueueStatus; costUsd? }   // costUsd present when a charge applied
interface QueueState { …; videoModel?: 'default' | 'next'; imageModel?: 'default' | 'next' }   // 'next' = a Gen8 Preview video clip / a Gen8 Preview images job
interface ApiErrorBody { error?; message?; [key: string]: unknown } // raw parsed error payload (err.details)

interface GenerationResult extends Creation { queueId; costUsd? }
interface Creation {
  creationId; prompt?; description?; toolKind?; style?; quality?; aiModel?;   // aiModel 'gen8.0' = rendered on a next channel
  videoModel?: 'default' | 'next'; imageModel?: 'default' | 'next';          // the channel that rendered the row (SDK β‰₯ 2.8.0)
  cdnId?; downloadUrl?; width?; height?; isUploaded?; source?;
  mediaKind?: 'image' | 'video' | 'audio'; duration?; hasAudio?;   // duration in seconds (clips and audio uploads); hasAudio: a Gen8 Preview video clip or an audio upload
  isCloned?; clonedFromCreationId?;         // single-creation fetches only
  folderId?; inTrash?; isPublicShared?;     // owner rows
  likes?; isLiked?; isOwner?;               // likes = total count; isLiked = the CALLING account
  nsfwFlagged?; nsfwRate?; username?;       // username on feed / shared-folder rows
  creationDate?; raw?
  // source: 'web' | 'sdk' | 'mcp' | 'api' β€” creation origin (SDK β‰₯ 2.0.1)
}
interface QueueState { queueId; status; quality?; videoModel?; imageModel?; costUsd?; creationId?; cdnId?; downloadUrl?; error?; errorCode?; errorMessage?; createdAt?; startedAt?; completedAt? }
// startedAt (SDK β‰₯ 2.8.0, server β‰₯ 2.5.0): when a worker picked the job up β€” absent while it still waits; with createdAt it gives the elapsed render time of a long Gen8 clip.
// errorCode / errorMessage (SDK β‰₯ 2.5.6): machine code + readable sentence on failed / timeout / expired jobs;
// error stays the raw outcome string. GenerationFailedError exposes the same errorCode / errorMessage getters.
type QueueStatus = 'pending'|'pendingCharge'|'processing'|'inProgress'|'completed'|'failed'|'cancelled'|'dismissed'|'timeout'|'expired'

interface Folder { folderId; title?; description?; icon?; kind?; isPublicShared?; isShared?; posterCreationId?; groupId?; isFavorite?; createdAt?; updatedAt?; raw? }
interface CreationPage { creations: Creation[]; page?; limit?; count?; nextBeforeDate? }  // count = rows in THIS page
interface FeedPage     { creations: Creation[]; page; limit; hasMore }
interface FolderPage   { folders: Folder[]; nextBeforeDate? }
type CdnRef = string | { cdnId?; outputCdnId?; downloadUrl? }
type CreationRef = string | { creationId? }

type AspectRatio = '1:1'|'16:9'|'9:16'|'4:3'|'3:4'|'3:2'|'2:3'

The stats and finance payload types (StatsOverview, StatsGenerations, StatsTop, FinanceWallet, AccountTier, WalletLockStatus, TransactionPage, BalanceHistory, CreditHistory, CreditSources, TokenPrice, TierInfo) are all exported too β€” their fields are listed in Stats & Wallet.

See Error Handling for the full error class table. For the underlying HTTP endpoints (any language), see REST API.

⚠️ **GitHub.com Fallback** ⚠️