Repository navigation
API Reference
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: stringclient.me(signal?): Promise<Account>-
client.download(ref, opts?: DownloadOptions)— shortcut forclient.cdn.download
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 creationSubmitOptions: { 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.
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).
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 idsinterface 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 ≤1000Combining 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.
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.
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).
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.
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, keywordsAll 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.
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 catalogueEverything 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.
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 | CdnFetchOptionsThe 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.
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.