ditinyAPI Reference

API Reference

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.

GETPOSTPATCHPUTDELETE

Authentication

POST/api/auth/registerNone

Register 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."
}
POST/api/auth/loginNone

Sign in and get JWT token

Request Body

{
  "email": "user@example.com",
  "password": "password"
}

Response

{
  "access_token": "eyJhbGciOiJIUzI...",
  "token_type": "bearer"
}
GET/api/auth/meBearer / API Key

Get 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"
}
PATCH/api/auth/meBearer

Update display name

Request Body

{
  "display_name": "New Name"
}
POST/api/auth/me/api-keyBearer

Generate API key (shown once)

Response

{
  "api_key": "64-char-hex-string...",
  "message": "Save this key now."
}
PUT/api/auth/me/api-key/domainsBearer

Set allowed domains for API key

Request Body

{
  "allowed_domains": ["example.com", "*.mysite.com"]
}
DELETE/api/auth/me/api-keyBearer

Revoke API key

GET/api/auth/me/webhookBearer / API Key

Get your file processing webhook setting

PUT/api/auth/me/webhookBearer / API Key

Configure 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"
}

Upload

POST/api/uploadBearer / API Key

Primary 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": {}
}
POST/api/upload/initBearer / API Key

Presigned 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
}
POST/api/upload/completeBearer / API Key

Confirm 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"
}

Upload Workflow

Files

GET/api/filesBearer / API Key

List 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
}
GET/api/files/{file_key}Bearer / API Key

Get file details with upload_status, thumbnails, attributes, and share info. share_token is null until upload complete.

GET/api/files/{file_key}/processing-statusBearer / API Key

Lightweight 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"
}
POST/api/files/{file_key}/processBearer / API Key

Dispatch 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"
}
PATCH/api/files/{file_key}Bearer / API Key

Update file (name, visibility)

Request Body

{
  "original_name": "new-name.jpg",
  "is_public": true
}
DELETE/api/files/{file_key}Bearer / API Key

Soft-delete a file

GET/api/files/{file_key}/downloadBearer / API Key

Get presigned download URL (302 redirect)

GET/api/files/{file_key}/download-urlBearer / API Key

Return JSON presigned download URL for browser clients that need Bearer auth before redirecting.

Response

{
  "url": "https://r2-presigned-url...",
  "expires_in": 900
}
GET/api/dashboard/statsBearer / API Key

Dashboard statistics (files, storage, categories)

Tags

GET/api/tagsBearer / API Key

List all tags

POST/api/tagsBearer

Create a tag (admin only)

Request Body

{
  "name": "important",
  "color": "#FF0000"
}
DELETE/api/tags/{tag_id}Bearer

Delete a tag (admin only)

POST/api/files/{file_key}/tagsBearer / API Key

Add tag to file

Request Body

{
  "tag_id": 1
}
DELETE/api/files/{file_key}/tags/{tag_id}Bearer / API Key

Remove tag from file

Share Links

GET/s/{share_token}None

Public file preview (no auth required). Available only after upload complete and if file is public. Returns preview_url plus thumbnail status/URLs when ready.

Response

{
  "file_key": "uuid",
  "original_name": "photo.jpg",
  "mime_type": "image/jpeg",
  "file_category": "image",
  "size_bytes": 1048576,
  "preview_url": "https://...",
  "thumbnails": [
    { "size_name": "sm", "status": "pending", "url": null },
    { "size_name": "md", "status": "ready", "url": "/s/{share_token}/thumb/md" }
  ],
  "is_downloadable": true
}
GET/s/{share_token}/thumb/{size}share_token (URL secret)

Permanent thumbnail URL (302 → Cloudflare Images) after deferred thumbnail is ready. Size: sm (300px), md (600px), lg (1200px). 404 if file deleted or thumbnail is pending/queued/processing/failed.

Thumbnails

GET/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.

Webhooks

POSTUser webhook URLHMAC / optional Bearer

Ditiny 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 }
  ]
}

Admin Ops

POST/api/admin/uploads/reconcile?limit=100Admin

Queue 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
}
POST/api/admin/outbox/dispatch?limit=100Admin

Queue outbox dispatch for recoverable post-upload jobs such as attributes and deferred thumbnails.

Response

{
  "status": "queued",
  "limit": 100
}
POST/api/admin/outbox/{job_id}/retryAdmin

Reset a failed/queued outbox job back to pending so the dispatcher can retry it.

Response

{
  "status": "pending",
  "job_id": 123
}
GET/api/admin/users/{user_id}/webhookAdmin

Get a user webhook setting

PUT/api/admin/users/{user_id}/webhookAdmin

Configure a user webhook setting

Upload Workflow

PRIMARY API

Multipart API Upload

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,
  ...
}

Python

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"])

Node.js

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();

PHP

$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.

Dispatch Processing After Multipart Upload

# 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()

Presigned URL Upload (Ditiny frontend / JWT)

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.

1

Initialize 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
}
2

Upload File to R2

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.

3

Confirm Upload

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"
}
4

Poll for Processing Status

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"
}

Complete Example — Presigned URL (Python, JWT/frontend flow)

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")

Complete Example — Presigned URL (JavaScript / JWT/frontend flow)

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");
}

Deferred Processing Expectations

File TypeThumbnailsAttributes
Image (JPEG/PNG)Deferred queueDeferred queue
Embroidery (DST/PES/JEF)Deferred queueDeferred queue
Embroidery (EMB/Wilcom)Deferred queueDeferred queue
PDFDeferred queueDeferred queue
Office (DOCX/XLSX/PPTX)Deferred queueN/A

Upload success does not wait for thumbnails. Treat thumbnails and attributes as eventually available.

Rate Limits

EndpointLimit
POST /api/upload600 req/min per user/API key, global cap 60000 req/min, max 50 concurrent per API process
POST /api/upload/initJWT/frontend flow only while API key presigned upload is paused
POST /api/upload/completeJWT/frontend flow only while API key presigned upload is paused
GET /api/files/*/download20 req/min
GET /s/{token}120 req/min
GET /s/{token}/thumb/*300 req/min
All other /api/*60 req/min

Supported File Formats

Images

JPEG, PNG, WebP, TIFF, GIF, SVG

Videos

MP4, MOV, AVI, MKV, WebM

Documents

PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX

Embroidery

EMB, DST, PES, JEF, VP3, HUS, EXP