The Ditiny API is a RESTful API for file storage, processing, and delivery. Authenticate with JWT tokens (from login) or API keys (from settings). All responses are JSON.
/api/auth/registerNoneRegister a new account (pending admin approval)
Request Body
{
"email": "user@example.com",
"password": "min8chars",
"display_name": "John Doe"
}Response
{
"message": "Registration successful. Your account is pending admin approval."
}/api/auth/loginNoneSign in and get JWT token
Request Body
{
"email": "user@example.com",
"password": "password"
}Response
{
"access_token": "eyJhbGciOiJIUzI...",
"token_type": "bearer"
}/api/auth/meBearer / API KeyGet current user profile
Response
{
"id": 1,
"email": "user@example.com",
"display_name": "John Doe",
"role": "user",
"is_active": true,
"created_at": "2026-03-19T00:00:00Z"
}/api/auth/meBearerUpdate display name
Request Body
{
"display_name": "New Name"
}/api/auth/me/api-keyBearerGenerate API key (shown once)
Response
{
"api_key": "64-char-hex-string...",
"message": "Save this key now."
}/api/auth/me/api-key/domainsBearerSet allowed domains for API key
Request Body
{
"allowed_domains": ["example.com", "*.mysite.com"]
}/api/auth/me/api-keyBearerRevoke API key
/api/auth/me/webhookBearer / API KeyGet your file processing webhook setting
/api/auth/me/webhookBearer / API KeyConfigure one user-level file.processed webhook. URL host must match allowed domains.
Request Body
{
"url": "https://example.com/ditiny-webhook",
"enabled": true,
"bearer_token": "optional-receiver-token",
"rotate_secret": false
}Response
{
"url": "https://example.com/ditiny-webhook",
"enabled": true,
"has_secret": true,
"has_bearer_token": true,
"signing_secret": "shown-on-create-or-rotate"
}/api/uploadBearer / API KeyPrimary API upload for X-API-Key clients. Multipart form field file, max 20MB, streamed through the API server to R2. Returns after upload finalize; processing is a separate step.
Request Body
curl -X POST https://i.ditiny.com/api/upload \ -H "X-API-Key: <key>" \ -F "file=@design.dst"
Response
{
"file_key": "uuid-string",
"upload_status": "uploaded",
"share_token": "xYz1234abc",
"original_name": "design.dst",
"file_category": "emb",
"size_bytes": 245760,
"storage_tier": "hot",
"thumbnails": [],
"stitch_count": null,
"color_count": null,
"file_attributes": {}
}/api/upload/initBearer / API KeyPresigned direct-to-R2 upload for Ditiny first-party frontend/JWT. Temporarily returns 503 for X-API-Key clients when PRESIGNED_UPLOAD_ENABLED=false.
Request Body
{
"original_name": "photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 1048576
}Response
{
"file_key": "uuid-string",
"status": "initialized",
"upload_url": "https://r2.cloudflarestorage.com/...",
"upload_headers": {
"Content-Type": "image/jpeg"
},
"expires_in": 1800
}/api/upload/completeBearer / API KeyConfirm presigned upload. Temporarily returns 503 for X-API-Key clients when PRESIGNED_UPLOAD_ENABLED=false.
Request Body
{
"file_key": "uuid-string"
}Response
{
"status": "uploaded",
"file_key": "uuid-string"
}/api/filesBearer / API KeyList files (own files for users, all for admin). Owner can see initialized/expired uploads with upload_status for retry or diagnosis.
Response
{
"items": [
{
"file_key": "uuid-string",
"upload_status": "uploaded",
"share_token": "xYz1234abc",
"thumbnails": []
}
],
"total": 100,
"page": 1,
"limit": 50,
"pages": 2
}/api/files/{file_key}Bearer / API KeyGet file details with upload_status, thumbnails, attributes, and share info. share_token is null until upload complete.
/api/files/{file_key}/processing-statusBearer / API KeyLightweight polling endpoint for thumbnail and attribute status. Does not write view access logs.
Response
{
"file_key": "uuid-string",
"upload_status": "uploaded",
"attributes_status": "succeeded",
"thumbnail_status": "processing",
"thumbnails": { "sm": "ready", "md": "processing", "lg": "queued" },
"ready": false,
"webhook_status": null,
"status": "processing"
}/api/files/{file_key}/processBearer / API KeyDispatch thumbnail and attribute jobs immediately. Use wait=true to wait up to timeout_seconds for completion. Returns the same ProcessingStatus shape.
Request Body
{
"wait": true,
"timeout_seconds": 60
}Response
{
"file_key": "uuid-string",
"upload_status": "uploaded",
"attributes_status": "queued",
"thumbnail_status": "queued",
"thumbnails": { "sm": "queued", "md": "queued", "lg": "queued" },
"ready": false,
"webhook_status": null,
"status": "queued"
}/api/files/{file_key}Bearer / API KeyUpdate file (name, visibility)
Request Body
{
"original_name": "new-name.jpg",
"is_public": true
}/api/files/{file_key}Bearer / API KeySoft-delete a file
/api/files/{file_key}/downloadBearer / API KeyGet presigned download URL (302 redirect)
/api/files/{file_key}/download-urlBearer / API KeyReturn JSON presigned download URL for browser clients that need Bearer auth before redirecting.
Response
{
"url": "https://r2-presigned-url...",
"expires_in": 900
}/api/dashboard/statsBearer / API KeyDashboard statistics (files, storage, categories)
/s/{share_token}/thumb/{sm|md|lg}share_token (URL secret)Only thumbnail delivery route. Ditiny validates share_token and file state, then redirects to Cloudflare Images. Output is WebP only; sizes are sm=300px, md=600px, lg=1200px.
User webhook URLHMAC / optional BearerDitiny sends file.processed after the file category's required processing jobs succeed. Payload includes status, attributes, and both relative paths and absolute URLs. Delivery is HMAC signed and may include your configured Bearer token.
Response
{
"event": "file.processed",
"status": "succeeded",
"file_key": "uuid-string",
"share_token": "xYz1234abc",
"file_category": "emb",
"attributes": { "stitch_count": 22258, "color_count": 6 },
"share": { "path": "/s/xYz1234abc", "url": "https://i.ditiny.com/s/xYz1234abc" },
"thumbnails": [
{ "size_name": "sm", "path": "/s/xYz1234abc/thumb/sm", "url": "https://i.ditiny.com/s/xYz1234abc/thumb/sm", "width_px": 300, "height_px": 240 }
]
}/api/admin/uploads/reconcile?limit=100AdminQueue a reconcile pass for initialized uploads. The worker HEADs R2: valid objects are finalized, missing objects expire, invalid object state fails for review.
Response
{
"status": "queued",
"limit": 100
}/api/admin/outbox/dispatch?limit=100AdminQueue outbox dispatch for recoverable post-upload jobs such as attributes and deferred thumbnails.
Response
{
"status": "queued",
"limit": 100
}/api/admin/outbox/{job_id}/retryAdminReset a failed/queued outbox job back to pending so the dispatcher can retry it.
Response
{
"status": "pending",
"job_id": 123
}/api/admin/users/{user_id}/webhookAdminGet a user webhook setting
/api/admin/users/{user_id}/webhookAdminConfigure a user webhook setting
Send the file directly to POST /api/upload as multipart form data. This endpoint is the primary flow for API key clients, accepts files up to 20MB, and streams the server-side file object to R2.
# Multipart API upload
curl -X POST https://i.ditiny.com/api/upload \
-H "X-API-Key: <your-api-key>" \
-F "file=@design.dst"
# Response: complete file record
{
"file_key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"upload_status": "uploaded",
"original_name": "design.dst",
"file_category": "emb",
"size_bytes": 245760,
"storage_tier": "hot",
"share_token": "xYz1234abc",
"thumbnails": [], // processing async
"stitch_count": null, // available after processing
"color_count": null,
...
}import requests
r = requests.post(
"https://i.ditiny.com/api/upload",
headers={"X-API-Key": "<key>"},
files={"file": open("design.dst", "rb")},
)
print(r.json()["file_key"])const form = new FormData();
form.append("file", fs.createReadStream("design.dst"));
const r = await fetch(
"https://i.ditiny.com/api/upload",
{
method: "POST",
headers: { "X-API-Key": "<key>" },
body: form,
}
);
const data = await r.json();$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "https://i.ditiny.com/api/upload",
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["X-API-Key: <key>"],
CURLOPT_POSTFIELDS => [
"file" => new CURLFile("design.dst")
],
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch));Upload returns as soon as the file is finalized in R2. Call POST /api/files/{file_key}/processto dispatch thumbnail and attribute jobs immediately. A configured user webhook is sent after the required processing for that file category is ready.
# Step 1: upload
upload = requests.post(
"https://i.ditiny.com/api/upload",
headers={"X-API-Key": "<key>"},
files={"file": open("design.dst", "rb")},
).json()
# Step 2: process now
status = requests.post(
f"https://i.ditiny.com/api/files/{upload['file_key']}/process",
headers={"X-API-Key": "<key>"},
json={"wait": False},
).json()
# Step 3: poll lightweight status
status = requests.get(
f"https://i.ditiny.com/api/files/{upload['file_key']}/processing-status",
headers={"X-API-Key": "<key>"},
).json()The first-party Ditiny frontend can continue using the 4-step presigned URL flow. X-API-Key clients receive 503 while PRESIGNED_UPLOAD_ENABLED=false and should use POST /api/upload.
Call POST /api/upload/init with file metadata. You'll receive a presigned URL for direct upload to Cloudflare R2.
// Request
POST /api/upload/init
Authorization: Bearer <token>
{
"original_name": "design.dst",
"mime_type": "application/octet-stream",
"size_bytes": 245760
}
// Response (200 OK)
{
"file_key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "initialized",
"upload_url": "https://r2.cloudflarestorage.com/bucket/path?X-Amz-Signature=...",
"upload_headers": {
"Content-Type": "application/octet-stream"
},
"expires_in": 1800
}Use the upload_url to PUT the file bytes directly to R2. This goes straight to storage — not through Ditiny's API server. Send every header returned in upload_headers; for presigned uploads this is only Content-Type.
// Upload directly to R2 (not to Ditiny API) PUT <upload_url from step 1> Content-Type: application/octet-stream <raw file bytes> // Response: 200 OK (from R2)
Note: The presigned URL expires in 30 minutes. Upload must complete within that window.
After the PUT succeeds, tell Ditiny the upload is done. Ditiny verifies the R2 object and records recoverable background jobs for thumbnail and attribute processing. Upload is complete at this point; processing runs separately.
// Request
POST /api/upload/complete
Authorization: Bearer <token>
{
"file_key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
// Response (200 OK)
{
"status": "uploaded",
"file_key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}Dispatch jobs with POST /api/files/{file_key}/process, then poll GET /api/files/{file_key}/processing-status. Thumbnails are WebP only and optimized for small file size.
// Dispatch now
POST /api/files/a1b2c3d4-e5f6-7890-abcd-ef1234567890/process
Authorization: Bearer <token>
{ "wait": false }
// Poll request
GET /api/files/a1b2c3d4-e5f6-7890-abcd-ef1234567890/processing-status
Authorization: Bearer <token>
// Response
{
"file_key": "a1b2c3d4-...",
"upload_status": "uploaded",
"attributes_status": "succeeded",
"thumbnail_status": "processing",
"thumbnails": {
"sm": "ready",
"md": "processing",
"lg": "queued"
},
"ready": false,
"status": "processing"
}import requests
import time
API = "https://i.ditiny.com"
HEADERS = {"Authorization": "Bearer <your-token>"}
# Presigned flow is for JWT/frontend while API key presigned upload is paused.
# Step 1: Init
init = requests.post(f"{API}/api/upload/init", json={
"original_name": "design.dst",
"mime_type": "application/octet-stream",
"size_bytes": 245760,
}, headers=HEADERS).json()
file_key = init["file_key"]
upload_url = init["upload_url"]
# Step 2: Upload to R2
with open("design.dst", "rb") as f:
r2 = requests.put(upload_url, data=f, headers=init["upload_headers"])
r2.raise_for_status()
# Step 3: Confirm
requests.post(f"{API}/api/upload/complete", json={
"file_key": file_key,
}, headers=HEADERS)
# Step 4: Dispatch processing, then poll lightweight status (max 2 minutes)
requests.post(f"{API}/api/files/{file_key}/process", json={
"wait": False,
}, headers=HEADERS)
for _ in range(40):
status = requests.get(
f"{API}/api/files/{file_key}/processing-status",
headers=HEADERS,
).json()
if status["ready"] or status["thumbnail_status"] == "failed":
print(f"Uploaded. Processing status: {status['status']}")
for size, thumb_status in status["thumbnails"].items():
print(f" {size}: {thumb_status}")
break
time.sleep(3)
else:
print("Uploaded; deferred processing is still pending")const API = "https://i.ditiny.com";
const headers = { "Authorization": "Bearer <your-token>" };
async function uploadFile(filePath) {
const fs = require("fs");
const fileBuffer = fs.readFileSync(filePath);
const fileName = filePath.split("/").pop();
// Step 1: Init
const init = await fetch(`${API}/api/upload/init`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
original_name: fileName,
mime_type: "application/octet-stream",
size_bytes: fileBuffer.length,
}),
}).then(r => r.json());
// Step 2: Upload to R2
const uploadResp = await fetch(init.upload_url, {
method: "PUT",
headers: init.upload_headers,
body: fileBuffer,
});
if (!uploadResp.ok) throw new Error("R2 upload failed");
// Step 3: Confirm
await fetch(`${API}/api/upload/complete`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ file_key: init.file_key }),
});
// Step 4: Dispatch processing, then poll lightweight status
await fetch(`${API}/api/files/${init.file_key}/process`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ wait: false }),
});
for (let i = 0; i < 40; i++) {
const status = await fetch(
`${API}/api/files/${init.file_key}/processing-status`, { headers }
).then(r => r.json());
if (status.ready || status.thumbnail_status === "failed") {
console.log("Upload complete; deferred processing state:", {
attributes: status.attributes_status,
thumbnails: status.thumbnails,
});
return status;
}
await new Promise(r => setTimeout(r, 3000));
}
console.log("Upload complete; deferred processing is still pending");
}| File Type | Thumbnails | Attributes |
|---|---|---|
| Image (JPEG/PNG) | Deferred queue | Deferred queue |
| Embroidery (DST/PES/JEF) | Deferred queue | Deferred queue |
| Embroidery (EMB/Wilcom) | Deferred queue | Deferred queue |
| Deferred queue | Deferred queue | |
| Office (DOCX/XLSX/PPTX) | Deferred queue | N/A |
Upload success does not wait for thumbnails. Treat thumbnails and attributes as eventually available.
| Endpoint | Limit |
|---|---|
| POST /api/upload | 600 req/min per user/API key, global cap 60000 req/min, max 50 concurrent per API process |
| POST /api/upload/init | JWT/frontend flow only while API key presigned upload is paused |
| POST /api/upload/complete | JWT/frontend flow only while API key presigned upload is paused |
| GET /api/files/*/download | 20 req/min |
| GET /s/{token} | 120 req/min |
| GET /s/{token}/thumb/* | 300 req/min |
| All other /api/* | 60 req/min |
JPEG, PNG, WebP, TIFF, GIF, SVG
MP4, MOV, AVI, MKV, WebM
PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX
EMB, DST, PES, JEF, VP3, HUS, EXP