Repository navigation
REST API
Everything the SDK does is plain HTTP — use it from Python, Go, Rust, curl or anything that speaks JSON. This page is the raw contract; if you're on JavaScript or TypeScript, the SDK wraps all of it (polling, retries, typed errors) and you can skip this.
-
Base URL —
https://dapp.2dai.io:444/v1 -
Auth —
Authorization: Bearer 2dai_sk_<keyId>_<secret>on every request. Header only; there is no query-param or cookie fallback. -
Content type —
application/jsonexceptPOST /v1/uploads(multipart/form-data). - User-Agent — optional, but it decides how your creations are labelled. See below.
Get a key in the dashboard: Integrations → API keys. See Keys & Scopes for the scope matrix and spend caps.
Every creation carries a source, and the dashboard uses it to file your work:
Cloud Drive → Activities has an SDK collection and an MCP collection, and
the lightbox shows a matching origin pill. The server derives source from your
User-Agent.
| You send | source |
Where it lands |
|---|---|---|
2dai-cloud-sdk/2.1.0 |
sdk |
SDK collection |
2dai-cloud-sdk/2.1.0 2dai-mcp-server/1.0.0 |
mcp |
MCP collection |
| anything else, or nothing | api |
neither — plain API history |
curl -X POST https://dapp.2dai.io:444/v1/generate/image \
-H "Authorization: Bearer $TWODAI_API_KEY" \
-H "User-Agent: 2dai-cloud-sdk/2.1.0" \
-H "Content-Type: application/json" \
-d '{"prompt":"a lighthouse at dusk"}'Omitting it is fine — nothing is gated on the header. The request succeeds,
the generation runs, you are charged the same. You simply land as api instead
of sdk, which is the honest label for a raw REST caller anyway. Send the SDK
token only if you genuinely want your traffic grouped with the SDK's.
The token must be a whole name/version word with a full x.y.z version.
These do NOT match, and fall back to api:
2dai-cloud-sdk ← no version
2dai-cloud-sdk/2.0 ← not x.y.z
2dai-cloud-sdk-fork/2.0.2 ← different name
my-app (uses 2dai-cloud-sdk)
Matching used to be a loose substring search, which mislabelled anything merely mentioning the name; it is strict as of the 2026-07-29 API build. This is attribution, never authorisation — no scope, quota or price depends on it, and forging a token only mislabels your own work in your own drive.
A generate call does not return an image. It returns a queueId; you then
poll GET /v1/queue/:queueId until status is completed, at which point the
row carries creationId, outputCdnId and downloadUrl.
# 1 · submit
curl -X POST https://dapp.2dai.io:444/v1/generate/image \
-H "Authorization: Bearer $TWODAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"a lighthouse at dusk","width":1456,"height":816}'
# → {"queueId":"a1b2…","status":"pending","costUsd":0.07}
# 2 · poll until completed
curl "https://dapp.2dai.io:444/v1/queue/a1b2…" \
-H "Authorization: Bearer $TWODAI_API_KEY"
# → {"queueId":"a1b2…","status":"completed","creationId":"…","downloadUrl":"/cdn/file/…"}
# 3 · fetch the bytes (same bearer)
curl -L "https://dapp.2dai.io:444/cdn/file/<outputCdnId>" \
-H "Authorization: Bearer $TWODAI_API_KEY" -o out.jpgPoll every 2 s or so. Terminal statuses: completed, failed, cancelled,
timeout, expired, dismissed. Non-terminal: pendingCharge, pending,
processing.
Every error is JSON with an error code, sometimes plus context fields:
{ "error": "KEY_SPEND_LIMIT_REACHED", "costUsd": 0.07, "spentUsd": 5, "spendLimitUsd": 5 }Three can come back from any endpoint:
| HTTP | Code | Meaning |
|---|---|---|
| 401 | INVALID_API_KEY |
Bad, malformed or revoked key |
| 401 | ACCOUNT_UNAVAILABLE |
Account can't use the app (banned, terms pending…) |
| 403 | MISSING_SCOPE |
Key lacks the scope; body carries requiredScope + scopes
|
Per-endpoint codes are listed below. Error Handling maps them to the SDK's typed classes.
JSON responses omit keys whose value is undefined — treat every optional field as "may be absent" rather than "may be null".
No parameters.
{
"userId": "0029e8ae…",
"username": "2DAI",
"creditUsd": 99855.65,
"tier": "tier5",
"key": { "keyId": "5zmvg7rj", "label": "E2E Tests",
"scopes": ["read","generate","manage","publish"],
"spendLimitUsd": null, "spentUsd": 1.36 }
}spendLimitUsd: null means uncapped. Your account credit is always the real
limit.
All three submit endpoints return the same shape:
{ "queueId": "…", "status": "pending", "costUsd": 0.07 }costUsd is present only when the call was charged.
| Field | Req | Type | Default | Notes |
|---|---|---|---|---|
prompt |
✅ | string | — | non-empty, ≤ 2000 chars |
style |
string | auto |
auto lets the server pick; raw disables styling |
|
quality |
string | auto |
fast, normal, high, max, ultra, ultimate (top two are tier-gated) — auto picks by tier: max, or a random max / ultra draw when your tier allows ultra; ultimate is never auto-picked, request it explicitly |
|
width / height
|
number | 1024 | clamped 1–8192 | |
negativePrompt |
string | — | ≤ 2000 chars | |
enhanced |
boolean | false |
rewrite the prompt with TIXI (the 2DAI prompt agent) before generating; ~2 s, no extra credit. Your text stays in prompt, the rewrite lands in finalPrompt
|
|
allowNSFW |
boolean | false |
requires the tier that permits it | |
refCreationIds |
string[] | — | up to 3 (strict — see below) | |
clientToken |
string | derived | idempotency; [0-9a-f]{1,64}
|
Every tool caps its reference list, and the caps are enforced strictly:
exceeding one is a 400 { "error": "TOO_MANY_REFS", "max": …, "got": … }
before any charge, rather than a silent trim.
| Endpoint / tool | Max refs |
|---|---|
generate/image (refCreationIds) |
3 |
face-ref / character-ref
|
6 |
style-transfer |
3 |
smart-edit |
4 (refs[0] = the image to edit) |
generate/wallpaper (refCreationIds) |
3 |
| Field | Req | Type | Notes |
|---|---|---|---|
tool |
✅ | enum |
face-ref | character-ref | style-transfer | smart-edit
|
refCreationIds |
✅ | string[] | ≥ 1; per-tool caps above. smart-edit: refs[0] is the image to edit, further refs are visual support |
prompt |
✅ | string | required in practice for all tools; for smart-edit it is the edit instruction |
quality, width, height, allowNSFW, clientToken
|
as above | ||
extractionDirective |
enum | style-transfer only: style (default) | composition | styleAndComposition
|
There is no
enhancedfield here: face-ref and character-ref always run TIXI server-side (their directives are built around it), so expect the same ~2 s of prompt-rewriting latency whether you ask for it or not. Style-transfer runs a two-step TIXI pass of its own.
Endpoint-specific errors: 400 INVALID_TOOL, 400 REFS_REQUIRED,
400 TOO_MANY_REFS (with max / got), 403 TOOL_NOT_ALLOWED_FOR_TIER,
400 INVALID_EXTRACTION_DIRECTIVE.
Wallpaper-resize: expand a stored creation into a wallpaper dimension.
| Field | Req | Type | Notes |
|---|---|---|---|
inputCreationId |
✅ | string | the creation to expand/resize |
dimension |
✅ | string |
standard | photo | widescreen | ultrawide — drives the price; unknown values are rejected (fail-closed) |
refCreationIds |
string[] | up to 3 extra soft-conditioning refs | |
prompt |
string | optional guidance for the expansion | |
allowNSFW, clientToken
|
as above |
Quality is forced to Ultra server-side (there is no quality field), same
as the studio's tool. Endpoint-specific errors: 400 INPUT_CREATION_REQUIRED,
400 MISSING_DIMENSION, 400 TOO_MANY_REFS.
| Field | Req | Type | Default | Notes |
|---|---|---|---|---|
inputCreationId |
✅ | string | — | the still to animate |
prompt |
string | auto | empty falls back to a motion default | |
(no enhanced) |
— | — | video prompts are never TIXI-rewritten on this surface | |
style |
string | auto |
motion styles | |
quality |
string | auto |
ultra / ultimate are tier-gated |
|
duration |
number | 6.5 |
one of 5, 6.5, 7.5 — 7.5 needs Supporter+ |
|
frameInterpolation |
boolean | false |
||
allowNSFW, clientToken
|
as above |
Endpoint-specific errors: 400 INPUT_CREATION_REQUIRED,
400 INVALID_INPUT_CREATION_ID, 400 INVALID_DURATION, 403 INSUFFICIENT_TIER.
Body: { "creationId": "…" }. The server re-derives the original tool's
submit from the stored row (prompt, style, quality, dimensions, references)
and queues it like any other generation — same ticket response, same pricing,
same gates, and the per-key spend cap applies. The new creation records its
parent in retriedFromCreationId.
Endpoint-specific errors: 400 MISSING_CREATION_ID,
404 CREATION_NOT_FOUND (unknown, not yours, or deleted),
400 CREATION_UNAVAILABLE (in trash),
400 REGEN_UNSUPPORTED_TOOL (raw upload or wallpaper-resize row),
400 REGEN_MISSING_SOURCE / 400 REGEN_MISSING_PROMPT (the stored row lacks
what the tool needs).
| HTTP | Code | Extra fields |
|---|---|---|
| 400 |
INVALID_PROMPT, INVALID_STYLE, INVALID_QUALITY
|
— |
| 402 | INSUFFICIENT_CREDIT |
costUsd, creditUsd, deficitUsd
|
| 402 | KEY_SPEND_LIMIT_REACHED |
costUsd, spentUsd, spendLimitUsd
|
| 403 | QUALITY_NOT_ALLOWED_FOR_TIER |
quality |
| 403 | NSFW_REFS_BLOCKED |
blockedCreationIds |
| 409 | QUEUE_LIMIT_REACHED |
maxPending, current
|
| 409 | IDEMPOTENT_RETRY |
— (your identical retry was de-duplicated) |
| 413 | STORAGE_QUOTA_EXCEEDED |
bytesUsed, quotaBytes
|
| 503 | PRICING_UNAVAILABLE |
— |
Both credit errors fire before any charge.
{
"queueId": "…", "status": "completed", "costUsd": 0.07,
"creationId": "…", "outputCdnId": "…", "downloadUrl": "/cdn/file/…",
"createdAt": "…", "completedAt": "…"
}error carries the failure reason on failed rows. 404 QUEUE_ITEM_NOT_FOUND
if the id is unknown or belongs to someone else.
Settled rows past your newest ~200 are eventually moved to an archive
(never deleted); this endpoint reads the archive transparently, so a stored
queueId keeps resolving with its terminal status and creationId.
Stop a generation that is still pending — the charge is refunded and the queue slot released:
{ "queueId": "…", "status": "cancelled" }Once a worker has picked the job up there is no abort hook:
409 { "error": "NOT_PENDING", "status": "processing" } with the current
status. The race is benign — either you cancelled in time and were refunded,
or the generation completes and you keep it. 404 QUEUE_ITEM_NOT_FOUND for
unknown ids, 403 when the item isn't yours.
The full listing engine — the same lenses and filters the studio's cloud
drive uses. POST /v1/creations mirrors it exactly with the params in a JSON
body, for when you'd rather keep search text out of URLs and proxy logs.
Modes — pick at most one; combining two is a
400 { "error": "CONFLICTING_FILTERS" }:
| Param | Values | Lens |
|---|---|---|
| (none) |
folderId (__root__ = ungrouped) · trashed (1/true/yes) |
the default flat listing |
activity |
all | history | uploaded | likes | public | trash | sdk | mcp | portfolio
|
the studio's fixed collections |
smart |
faces | videos | crop | alpha | favorites
|
smart collections (favorites = creations inside starred folders) |
sharedFolderId |
a folder id | a folder shared with you (view access) |
groupId |
a folder-group id | every folder filed in that group, one listing (your own groups — see GET /v1/folder-groups); 404 GROUP_NOT_FOUND / 403 NOT_AUTHORIZED
|
Cross-cutting params — compose with any mode:
| Param | Notes |
|---|---|
search |
whole-word AND over descriptions + tags; a trailing * makes a token a prefix (cur*); ≤ 128 chars / 8 tokens |
sort |
newest (default) | oldest | updated | size-desc | size-asc | type-asc | type-desc
|
ratioFilter |
square | landscape | portrait, or a comma set (square,portrait); unknown ids are dropped |
from / to
|
date range — ISO string or epoch ms |
hideFiled |
1 — exclude creations already filed into a folder (history/uploaded/likes/sdk/mcp lenses only) |
usedRef |
a creation id — only creations BUILT FROM it: any reference slot or lineage parent counts, clone families expanded; 400 INVALID_USED_REF on a malformed id |
blockNSFW |
1 — exclude NSFW-flagged rows (your account-level preference applies regardless) |
random |
1 — return { "creation": Row | null } instead of a page: ONE uniformly-sampled creation from the whole filtered collection. Not available on the trash (400 RANDOM_NOT_SUPPORTED) |
limit / page / beforeDate
|
limit 1..100 default 20 · page 1-based · beforeDate cursor |
{
"creations": [{
"creationId": "…", "prompt": "…", "description": "…",
"toolKind": "image-gen", "style": "…", "quality": "high", "aiModel": "gen7.2",
"outputCdnId": "…", "downloadUrl": "/cdn/file/…",
"width": 2048, "height": 2048,
"isUploaded": false, "source": "sdk", "creationDate": "…",
"folderId": "…", "inTrash": false, "isPublicShared": false,
"likes": 3, "isLiked": true, "isOwner": true,
"nsfwFlagged": false, "nsfwRate": 0.1, "username": "…"
}],
"page": 1, "limit": 20, "count": 20,
"nextBeforeDate": "…"
}count is the rows in this page, not the collection total.
nextBeforeDate is present only while a further page may exist and the
rows are in newest-first date order (the default sort, no relevance-ranked
search) — feed it back as beforeDate; for any other ordering, page with
page. prompt, nsfwRate and folderId are owner-only; likes is the
total count and isLiked is whether your account liked the row. source
is web (studio), sdk, mcp or api.
The cross-user public feed, newest-published first. Query: limit (1..48,
default 24) · page (1-based, ≤ 1000) · includeNsfw (1 to include
flagged rows).
{ "creations": [ … ], "page": 1, "limit": 24, "hasMore": true }Rows are the same shape as above; rows you don't own hide their prompts.
Returns { "creation": { … } } — the full owner view: prompts, dimensions,
references, NSFW rating, lineage, folder, timestamps, plus downloadUrl.
Note the key is creation.id here, not creationId.
404 CREATION_NOT_FOUND when unknown, not yours, or deleted.
| Endpoint | Body | Returns |
|---|---|---|
POST /v1/creations/:id/move |
{ folderId } — null/__root__ detaches |
{ creationId, folderId } |
POST /v1/creations/:id/trash |
— | { creationId, trashed: true } |
POST /v1/creations/:id/restore |
— | { creationId, trashed: false } |
DELETE /v1/creations/:id |
— | { creationId, deleted: true } |
POST /v1/creations/:id/clone |
— |
{ creation } (full owner view of the copy) |
Permanent delete requires the creation to be in trash first, else
409 NOT_IN_TRASH. Other codes: 404 CREATION_NOT_FOUND, 403 NOT_AUTHORIZED,
410 ALREADY_DELETED, 403 INVALID_FOLDER.
Clone makes a byte-identical duplicate (images only) with its own CDN file,
inheriting the original's description/tags/moderation verdict; no credit
cost, but the upload tier + storage-quota gates apply. Clone-specific codes:
403 CLONE_FORBIDDEN, 400 CLONE_UNSUPPORTED_MEDIA (video/gif),
409 CREATION_HAS_NO_MEDIA, 403 UPLOAD_NOT_ALLOWED_FOR_TIER,
413 STORAGE_QUOTA_EXCEEDED.
| Endpoint | Body | Returns |
|---|---|---|
POST /v1/creations/:id/like |
{ "liked": true | false } |
{ liked, likeCount, changed } |
POST /v1/creations/reorder |
{ "creationIds": [ … ] } (≤ 1000) |
{ reordered } |
POST /v1/creations/batch |
{ "action": "trash" | "untrash" | "delete", "ids": [ … ] } (≤ 1000) |
{ action, requested, processed, skipped } |
like is an idempotent SET, not a toggle — retries are safe, and
changed: false means the state already matched. Any live creation is
likeable; trashed/deleted rows answer 410. reorder stamps
creationIds[i] with sort index i (ids you don't own are silently
skipped). batch is owner-only; rows whose state doesn't fit the action are
counted in skipped — delete requires rows already in trash.
| Endpoint | Scope | Returns |
|---|---|---|
POST /v1/creations/:id/flag-nsfw |
manage |
{ nsfwFlagged: true, nsfwManual: "nsfw", nsfwRate } |
POST /v1/creations/:id/explain-nsfw |
generate |
{ explanation, cached, nsfwRate, nsfwThreshold } |
POST /v1/creations/:id/score-face-ref |
generate |
{ score, scoredAt, cached } |
POST /v1/creations/:id/score-character-ref |
generate |
{ score, scoredAt, cached } |
All four are owner-only. flag-nsfw is a manual safe → NSFW escalation (the
reverse direction isn't exposed here) and flips the CDN gate immediately.
The other three are cached on the row: the first call burns a vision-LLM
pass (hence the generate scope), repeats answer from the stored result with
cached: true. Codes: 400 NOT_FLAGGED (explain on a clean row),
422 SCORE_PARSE_FAILED, 503 = the LLM pass didn't land — retry later.
| Endpoint | Returns |
|---|---|
POST /v1/creations/:id/publish |
{ creationId, isPublicShared: true } |
POST /v1/creations/:id/unpublish |
{ creationId, isPublicShared: false } |
409 CREATION_IN_TRASH — restore it first.
409 UPLOADED_NOT_PUBLISHABLE — uploaded source files can't be published, only
your own generations.
multipart/form-data with a single file field named file, plus optional
text fields:
| Field | Meaning |
|---|---|
targetFolderId |
file the upload straight into this folder (instead of the drive root). Must be a folder you can write to — yours, or shared with you with editor rights; system folders excluded. Otherwise 403 INVALID_FOLDER and the upload is discarded. The duplicate check below is scoped to this folder |
croppedFromCreationId |
record the upload as a crop of a creation you own (images only) |
erasedFromCreationId |
record the upload as an erase edit — mutually exclusive with croppedFromCreationId
|
curl -X POST https://dapp.2dai.io:444/v1/uploads \
-H "Authorization: Bearer $TWODAI_API_KEY" \
-F "file=@./ref.jpg" \
-F "targetFolderId=$FOLDER_ID"Accepted types: JPEG, PNG, WebP, GIF, MP4 (video uploads are tier-gated). Per-account size cap applies (10 MB on the base tier).
Returns the created creation object directly (not wrapped).
Notable errors: 400 NO_FILE, 400 INVALID_MIME, 403 UPLOAD_NOT_ALLOWED_FOR_TIER,
403 VIDEOS_NOT_ALLOWED, 403 NSFW_REJECTED, 403 INVALID_FOLDER,
413 FILE_TOO_LARGE, 413 STORAGE_QUOTA_EXCEEDED, 409 DUPLICATE_IN_FOLDER
(identical bytes already in that folder), 429 UPLOAD_IN_PROGRESS.
Advisory pre-flight dedup: does a creation with this content hash already
exist? Query: md5 (32 hex chars — the md5 of the file bytes) · folderId
(optional; omit to check the drive root).
{ "duplicate": true, "existingCreationId": "…", "folderId": "…" }Saves the full upload round-trip, but the upload itself still re-checks — a
concurrent upload can race a false, so treat it as a hint, not a lock.
| Endpoint | Scope | Body / query | Returns |
|---|---|---|---|
GET /v1/folders |
read |
limit (default 20, max 100) · beforeDate (cursor) · sort
|
{ folders: [...], nextBeforeDate } |
POST /v1/folders |
manage |
{ title, description?, groupId? } — title ≤ 40, description ≤ 500; groupId must be one of your groups |
{ folder } |
PATCH /v1/folders/:id |
manage |
{ title?, description?, groupId?, posterCreationId?, isFavorite? } — at least one. groupId: string sets the group, null detaches. posterCreationId: string pins the poster, null clears. isFavorite: boolean |
{ folder } |
DELETE /v1/folders/:id |
manage |
?trashContents=1 also trashes the contents |
{ folderId, deleted: true } |
An account holds up to 500 folders — 409 FOLDER_LIMIT_REACHED past the
cap. Folder shape:
{ "folderId": "…", "title": "…", "description": "…", "icon": "…",
"kind": "private", "isPublicShared": false, "isShared": false,
"posterCreationId": "…", "groupId": "…", "isFavorite": false,
"createdAt": "…", "updatedAt": "…" }groupId is absent when the folder is ungrouped; isFavorite marks the
sidebar star (starred folders feed the smart=favorites lens).
The list is newest-first by createdAt by default, and nextBeforeDate is
present only when a further page may exist — feed it back as beforeDate, same
as GET /v1/creations.
sort accepts the same vocabulary as the dashboard: index, date-desc,
date-asc, name-asc, name-desc, update-desc. Note the interaction with the
cursor: beforeDate filters on the creation date, so it can only page a
date-descending sequence without skipping or repeating rows. Ask for
name-asc (or any non-date order) and you get your ordering, but
nextBeforeDate is omitted — raise limit instead, which at 100 covers any
realistic number of collections. The alternative, accepting sort and handing
back a cursor that silently corrupts the sequence, would be the worse contract.
Errors: 400 INVALID_TITLE (with maxLength), 400 NO_UPDATES,
400 FIXED_KIND_PROTECTED (system folders can't be renamed or deleted),
404 NOT_FOUND, 403 NOT_AUTHORIZED.
Groups are the collapsible sections the studio's cloud-drive sidebar organises collections into. They hold folders, never creations directly:
| Endpoint | Scope | Body | Returns |
|---|---|---|---|
GET /v1/folder-groups |
read |
— |
{ groups: [...] } — sidebar order |
POST /v1/folder-groups |
manage |
{ title } |
{ group } |
PATCH /v1/folder-groups/:id |
manage |
{ title } |
{ group } |
DELETE /v1/folder-groups/:id |
manage |
— | { groupId, deleted: true } |
Group shape: { groupId, title, sortIndex, collapsed, createdAt, updatedAt }.
An account holds up to 100 groups (409 GROUP_LIMIT_REACHED). Deleting a
group only detaches its member folders back to the ungrouped pane —
folders and creations are never deleted through this route. Move folders in
and out via PATCH /v1/folders/:id with groupId.
Read-only account analytics, all scope read. overview and top are
served from a server-side cache; their cache block
({ computedAt, ageSeconds, stale }) says how fresh the numbers are —
stale: true means a background refresh was kicked, re-fetch shortly.
{
"storage": { "usedBytes": 0, "quotaBytes": 0, "imageBytes": 0,
"videoBytes": 0, "uploadBytes": 0, "trashBytes": 0 },
"counters": { "totalCreations": 0, "weekCreations": 0, "weekDelta": 0,
"privateFolders": 0, "sharedFolders": 0,
"favoriteFolders": 0, "trashCreations": 0 },
"streak": { "days": 0 },
"smartSummaries": [{ "id": "faces", "count": 0 }],
"toolsRecent": { "image-gen": { "total": 0, "recentIds": ["…"] } },
"cache": { "computedAt": "…", "ageSeconds": 12, "stale": false }
}Generation volume + spend over the window (any other days is a 400); a live
aggregate memoized ~60 s per window:
{
"days": 30,
"totals": { "count": 0, "costUsd": 0 },
"byDay": [{ "day": "2026-08-01", "count": 0, "costUsd": 0 }],
"byTool": [{ "toolKind": "image-gen", "count": 0, "costUsd": 0 }],
"bySource": { "machine": { "count": 0, "costUsd": 0 },
"studio": { "count": 0, "costUsd": 0 } }
}machine is work made through keys (SDK / MCP / raw REST); studio is the
web app.
Most-used references (with the config the studio would suggest for each), styles and keywords:
{
"topRefs": [{ "referenceCreationId": "…", "useCount": 0,
"suggestedTool": "character-ref", "latestUsedAt": "…",
"suggestedConfig": { "quality": "high", "style": null,
"motionStyle": null, "requestedDuration": null,
"isEnhanced": false, "frameInterpolation": false } }],
"topStyles": [{ "style": "…", "count": 0 }],
"topKeywords": [{ "tag": "…", "count": 0 }],
"cache": { "computedAt": "…", "ageSeconds": 12, "stale": false }
}The money family is read-only by construction: no deposit, withdraw, swap
or lock operation exists anywhere on the API-key surface — those live in the
dashboard. The wallet reads require the opt-in finance scope (never in the
mint defaults); the two catalogue reads only need read.
| Endpoint | Returns |
|---|---|
GET /v1/tiers |
{ tiers: [{ key, name, valueUSD, icon?, color?, settings? }, …] } — the public tier catalogue |
GET /v1/token/price |
{ usdPrice, asOf, staleMs } — the cached $2DAI quote (machine calls never trigger a live refresh). 503 PRICE_UNAVAILABLE when no cache exists yet |
| Endpoint | Query | Returns |
|---|---|---|
GET /v1/wallet |
— | { tokens, creditUsd, tier, pendingWithdrawal, pendingSwap, lockActive } |
GET /v1/account/tier |
— |
{ effective, legs: { db, watermark, holdWatermark, dynamic } } — the effective tier is the max of the 4 legs |
GET /v1/wallet/lock |
— |
{ kind: "none" }, or { kind: "active" | "expiring-soon", lock, remainingMs }
|
GET /v1/wallet/transactions |
limit (5..50, default 20) · page (0-based) |
{ transactions: [{ id, type, status, asset, amount, hash, date, processedDate, counterparty, reason, needsAdminReview, secondaryUsd }], page, pageSize, hasMore } |
GET /v1/wallet/balance-history |
limit = days (7..365, default 90) |
{ currentBalance, airdropUsdCredit, points: [{ date, balance, delta, type }] } |
GET /v1/wallet/credit-history |
limit = days (7..365, default 90) |
{ currentCredit, tokenBalance, points: [{ date, balance, delta, type }] } |
GET /v1/wallet/credit/sources |
days (1..365, default 30) |
{ days, accruedUsd, bonusesUsd, swappedUsd, spentUsd } |
amount is a magnitude — the direction comes from type. secondaryUsd is
set on swap rows only. A key without the scope gets
403 { "error": "MISSING_SCOPE", "requiredScope": "finance", "scopes": [ … ] }.
Send the same bearer; the response is the raw bytes (not JSON), with
content-type, content-length and etag passed through.
curl -L "https://dapp.2dai.io:444/cdn/file/<outputCdnId>" \
-H "Authorization: Bearer $TWODAI_API_KEY" -o out.jpgYou can append an extension (/cdn/file/<id>.jpg) and pass CDN transform
params: ?s=<px> scales the longest edge keeping the aspect ratio (what you
want for previews), ?w=<px> / ?h=<px> target one axis and may crop. ?s
takes precedence over ?w / ?h when both are sent.
curl -L "https://dapp.2dai.io:444/cdn/file/<id>?s=512" \
-H "Authorization: Bearer $TWODAI_API_KEY" -o preview.jpgPublicly-shared creations serve without auth; everything else requires the key
that owns them. 404 for unknown ids, 403 when the key may not see that file.
-
Pending generations are capped per tier (4 on the base tier) —
409 QUEUE_LIMIT_REACHEDwhen you exceed it. Poll and retry. -
Pagination —
limitmaxes out at 100 (48 on the feed; 50 on wallet transactions). -
References are capped per tool (strict
400 TOO_MANY_REFS— see the caps table above). -
Bulk —
reorderandbatchtake at most 1000 ids per call. -
Drive — 500 folders and 100 folder groups per account
(
409 FOLDER_LIMIT_REACHED/GROUP_LIMIT_REACHED). - JSON bodies are capped at 5 MB; prompts at 2000 characters.
-
Idempotency — submits carry a
clientToken. Re-sending an identical call within a few seconds returns409 IDEMPOTENT_RETRYinstead of charging twice. Pass your own token to control the window. -
Rate/abuse — repeated auth failures and 404s count against your IP; a
blocked IP gets
429with an empty body on every route.