The Ditiny API is a RESTful API for file storage, processing, and delivery. Authenticate with JWT tokens (from login) or API keys (from settings). Most API responses are JSON; documented exceptions include the HTML share page, 204 responses, and 302 redirects. Proxy-generated errors may be plain text or HTML. Response examples below may be excerpts; treat the documented schemas as authoritative.
Origin or Referer whose host matches that list. Server-to-server requests without a matching header receive 403. An empty list applies no domain restriction; use an exact domain instead of * whenever possible. Because server clients can synthesize these headers, this is not a strong security boundary or a substitute for protecting the API key. Webhook URLs also require a configured matching allowed domain./healthNoneBasic database and Redis health snapshot. Use GET: curl -I sends HEAD, which is not implemented and returns 405. HTTP 200 can still contain status=degraded, so inspect the JSON fields.
Request example / body
curl https://i.ditiny.com/health
Response example / excerpt
{
"status": "ok",
"version": "0.1.0",
"db": "ok",
"redis": "ok"
}/api/auth/loginNoneSign in and get JWT token
Request example / body
{
"email": "user@example.com",
"password": "password"
}Response example / excerpt
{
"access_token": "eyJhbGciOiJIUzI...",
"token_type": "bearer"
}/api/upload/modesBearer / API KeyDiscover the account-assigned upload mode, enabled transports, maximum size, endpoint, upload host, and connection requirement. Call this before choosing an upload flow.
Response example / excerpt
{
"preferred_mode": "ditiny_gateway",
"fallback_mode": "multipart",
"max_file_size_bytes": 52428800,
"modes": [
{ "mode": "multipart", "enabled": true, "upload_host": "i.ditiny.com" },
{ "mode": "native_presigned", "enabled": false, "upload_host": "<account-id>.r2.cloudflarestorage.com" },
{ "mode": "ditiny_gateway", "enabled": true, "upload_host": "uploads.i.ditiny.com" }
]
}/api/upload/initBearer / API KeyInitialize native_presigned or ditiny_gateway upload. For an API key, mode defaults to and must match the administrator-assigned account mode. Multipart accounts use POST /api/upload instead.
Request example / body
{
"original_name": "photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 1048576,
"mode": "ditiny_gateway"
}Response example / excerpt
{
"file_key": "uuid-string",
"upload_mode": "ditiny_gateway",
"upload_url": "https://uploads.i.ditiny.com/v1/files/uuid?token=<opaque>",
"upload_host": "uploads.i.ditiny.com",
"upload_headers": { "Content-Type": "image/jpeg" },
"complete_url": "https://i.ditiny.com/api/upload/complete",
"processing_status_url": "https://i.ditiny.com/api/files/uuid/processing-status",
"status": "initialized",
"expires_in": 1800
}{upload_url}Opaque upload URL onlyUpload raw bytes to the exact URL returned by /init with every returned upload header. Never send the Ditiny API key or JWT. Gateway mode requires exact Content-Length and returns 201; native mode sends directly to R2.
Request example / body
curl -X PUT "<upload_url>" \ -H "Content-Type: image/jpeg" \ --data-binary "@photo.jpg"
/api/upload/completeBearer / API KeyFinalize a native or gateway session after PUT. Ditiny verifies the R2 object and returns without waiting for thumbnail or attribute processing. Repeating complete for an already-finalized file is safe.
Request example / body
{
"file_key": "uuid-string"
}Response example / excerpt
{
"status": "uploaded",
"file_key": "uuid-string"
}/api/uploadBearer / API KeyMultipart compatibility flow. Send multipart/form-data with field file, up to 50 MiB. Ditiny streams the file through the API server to R2 and returns after upload finalization; processing remains asynchronous.
Request example / body
curl -X POST https://i.ditiny.com/api/upload \ -H "X-API-Key: <key>" \ -H "Origin: https://your-configured-domain.example" \ -F "file=@design.dst"
Response example / excerpt
{
"file_key": "uuid-string",
"upload_mode": "multipart",
"upload_status": "uploaded",
"share_token": "xYz1234abc",
"is_public": true,
"original_name": "design.dst",
"file_category": "emb",
"size_bytes": 245760,
"storage_tier": "hot",
"thumbnails": [],
"stitch_count": null,
"color_count": null,
"file_attributes": {}
}GET /api/upload/modes and follow its enabledpreferred_mode. The API-key allowed-domain list checksOrigin/Referer on Ditiny API calls; it is separate from your company's outbound firewall allowlist. Never forward a Ditiny API key or JWT to the returned upload URL.Send the file directly to POST /api/upload as multipart form data. This compatibility mode accepts files up to 50 MiB and streams the file through the Ditiny API server to R2. Allow outbound HTTPS to i.ditiny.com. Do not call /initwhen your assigned mode is multipart.
# 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",
"is_public": true,
"thumbnails": [],
"stitch_count": null,
"color_count": null,
"file_attributes": {}
}PATCH /api/files/{file_key}with { "is_public": false }. This does not eliminate the public window between upload finalization and a successful PATCH. Do not upload secrets or data that must be private-at-create through the current API. The returned share token and thumbnail URLs are capability secrets; do not expose them unintentionally.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. Prefer wait=false and poll the lightweight status endpoint. If you use wait=true through the public host, keep timeout_seconds at 55 or less.
# 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()Fastest path: bytes never cross the Ditiny app server. Allow HTTPS to i.ditiny.com and the exact upload_host returned by each /init response. Browser clients also need an R2 CORS rule allowing their origin, PUT, and Content-Type.
# 1. Initialize with your Ditiny API key
POST https://i.ditiny.com/api/upload/init
{"original_name":"photo.jpg","mime_type":"image/jpeg","size_bytes":1048576,"mode":"native_presigned"}
# 2. PUT raw bytes to the exact upload_url
curl -X PUT "<upload_url>" \
-H "Content-Type: image/jpeg" \
--data-binary "@photo.jpg"
# 3. POST complete_url with Ditiny auth
{"file_key":"<file_key>"}Use this when your firewall cannot allowlist R2. Allow HTTPS to i.ditiny.com and the fixed host uploads.i.ditiny.com. The gateway validates the short-lived URL and streams bytes into R2; send exact Content-Type and Content-Length. Browser CORS is handled by the gateway.
# 1. Initialize
POST https://i.ditiny.com/api/upload/init
{"original_name":"photo.jpg","mime_type":"image/jpeg","size_bytes":1048576,"mode":"ditiny_gateway"}
# 2. PUT raw bytes; no Ditiny auth header
curl -X PUT "<upload_url>" \
-H "Content-Type: image/jpeg" \
--data-binary "@photo.jpg"
# 3. POST complete_url, then poll processing_status_url
{"file_key":"<file_key>"}| File Type | Thumbnails | Attributes |
|---|---|---|
| Raster image | sm, md, lg | Deferred queue |
| SVG | Not guaranteed | Not guaranteed |
| Embroidery | sm, md, lg | Deferred queue |
| sm, md, lg | Deferred queue | |
| Office (DOC/XLS/PPT) | sm, md, lg | N/A |
| Video | N/A | Deferred queue; attributes may be empty |
| Other | N/A | N/A |
Upload success does not wait for post-processing. A thumbnail-capable file is ready only after sm, md, and lg are ready and all required jobs have succeeded.
Metadata extraction is asynchronous. Wait for attributes_status=succeeded, then read GET /api/files/{file_key} or the file.processed webhook.
{
"width": 52,
"height": 14,
"stitch_count": 1770,
"color_count": 1,
"file_attributes": {
"source_format": ".emb",
"container_type": "wilcom_emb",
"width_mm": 52.19,
"height_mm": 13.8,
"dim_source": "wilcom_props",
"stitch_count": 1770,
"color_count": 1,
"thread_changes": 0,
"trim_count": 7,
"object_count": 1,
"machine_type": "Tajima",
"design_type": "Native design",
"has_embedded_preview": true,
"thread_palette": [
{
"index": 1,
"stitches": 1770,
"name": "Black",
"brand": "Default",
"hex": "#000000"
}
]
}
}For Wilcom EMB, Ditiny reads width and height directly from design properties in 1/180 mm units. Use the floating-point file_attributes.width_mm and height_mm values for hoop checks or costing. Top-level width and height are rounded millimetres kept for compatibility.
| Source | Meaning |
|---|---|
| wilcom_props | Dimensions stored in the Wilcom EMB metadata. |
| thumbnail_est | Approximate fallback when the Wilcom properties are absent. |
| key omitted | DST/PES/JEF and similar formats use the stitch bounding box. |
thread_changes, trim_count, object_count, machine_type, design_type, and thread_palette when present. Full multiple colorways, ordered stop-sequence records, catalog codes/thread lengths, EMB version/grade, appliqué, sequin, bling/bead, and lettering/monogram/order metadata are not currently returned./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 example / excerpt
{
"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. For embroidery, prefer file_attributes.width_mm and height_mm over the rounded legacy width and height fields. 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 example / excerpt
{
"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 required thumbnail and attribute jobs immediately. Prefer wait=false and poll processing-status. For a synchronous public request, use wait=true with timeout_seconds no greater than 55 seconds to stay below the 60-second proxy timeout.
Request example / body
{
"wait": true,
"timeout_seconds": 55
}Response example / excerpt
{
"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 example / body
{
"original_name": "new-name.jpg",
"is_public": true
}/api/files/{file_key}Bearer / API KeySoft-delete a file. Fresh share and thumbnail gateway requests then return 404/410, but cached redirects or a previously copied direct Cloudflare Images URL may remain usable. The public API has no restore endpoint.
/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 example / excerpt
{
"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)Stable capability URL that redirects to a ready WebP thumbnail in Cloudflare Images. Sizes are sm=300px, md=600px, and lg=1200px. Anyone holding the URL can access the thumbnail even when is_public=false; keep share_token secret. A fresh gateway request returns 404 while the thumbnail is not ready or after soft-delete, but cached redirects and copied direct Cloudflare Images URLs are not revoked immediately.
/api/auth/me/webhookBearer / API KeyGet your file processing webhook setting.
/api/auth/me/webhookBearer / API KeyConfigure one user-level file.processed webhook. The URL must use HTTPS; literal localhost/private/reserved IP hosts are rejected, but this is not complete DNS-rebinding protection. Its host must match a configured API-key allowed domain.
Request example / body
{
"url": "https://example.com/ditiny-webhook",
"enabled": true,
"bearer_token": "optional-receiver-token",
"rotate_secret": false
}Response example / excerpt
{
"url": "https://example.com/ditiny-webhook",
"enabled": true,
"has_secret": true,
"has_bearer_token": true,
"signing_secret": "shown-on-create-or-rotate"
}User webhook URLHMAC / optional BearerDitiny sends file.processed after required jobs succeed. Category other emits no event, and configuring a webhook after a file is already ready does not backfill it. The current HMAC signs timestamp plus Python canonical JSON (json.dumps(parsed_body, separators=(",", ":"), sort_keys=True)), which may differ from the raw HTTP body; hashing raw bytes can fail. A private file payload can still contain a share URL that returns 403.
Response example / excerpt
{
"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/files/{file_key}/webhook-deliveriesBearer / API KeyList webhook delivery history for a file owned by the current user. Admins can also inspect any file.
Response example / excerpt
[
{
"id": 123,
"file_key": "uuid-string",
"event_type": "file.processed",
"status": "succeeded",
"attempts": 1,
"next_attempt_at": "2026-05-23T00:00:00Z",
"last_status_code": 200,
"last_error": null,
"created_at": "2026-05-23T00:00:00Z",
"updated_at": "2026-05-23T00:00:03Z"
}
]| Endpoint | Limit |
|---|---|
| POST /api/upload | 600 req/min per IP and per user/API key; global application cap 60,000 req/min; max 50 concurrent per API process |
| POST /api/upload/init, /complete | 120 req/min per user; global application cap 6,000 req/min through Redis |
| GET /api/files/*/download* | 600 req/min per IP on the canonical host |
| GET /s/{token}, /json | 600 req/min per IP |
| GET /s/{token}/thumb/* | 1,800 req/min per IP |
| All other /api/* | 600 req/min per IP |
These are sustained proxy ceilings; burst and application-level controls also apply. On 429 or a transient 503, honorRetry-Afterwhen present, otherwise retry with bounded exponential backoff and jitter.
| Status | Meaning and action |
|---|---|
| 400 / 422 | Invalid request or validation error; correct the request before retrying. |
| 401 | Missing or invalid JWT/API key. |
| 403 | Inactive account, insufficient role, private share, or API-key domain mismatch. |
| 404 / 410 | Resource not found, thumbnail not ready, or share file deleted. |
| 409 | Upload state/object validation conflict; inspect detail before retrying. |
| 413 / 415 | File is too large or its MIME type is unsupported; do not retry unchanged. |
| 429 / 503 | Rate limit, disabled flow, or temporary capacity issue; apply bounded backoff and honor Retry-After when provided. |
FastAPI errors normally use { "detail": ... }. Proxy-generated errors may use another content type. Include the X-Request-ID response header when contacting support, when present.
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