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

API key domain rules: when an API key has a non-empty allowed-domain list, every request must include an 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.
GETPOSTPATCHPUTDELETE

Health

GET/healthNone

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

Authentication

POST/api/auth/loginNone

Sign in and get JWT token

Request example / body

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

Response example / excerpt

{
  "access_token": "eyJhbGciOiJIUzI...",
  "token_type": "bearer"
}

Upload

GET/api/upload/modesBearer / API Key

Discover 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" }
  ]
}
POST/api/upload/initBearer / API Key

Initialize 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
}
PUT{upload_url}Opaque upload URL only

Upload 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"
POST/api/upload/completeBearer / API Key

Finalize 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"
}
POST/api/uploadBearer / API Key

Multipart 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": {}
}

Upload Workflow

Start with 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.
MODE 1 / FALLBACK

Multipart API Upload

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": {}
}
Visibility warning: uploads are public by default. If a file must be private, immediately callPATCH /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.

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

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()
MODE 2

Native Presigned R2

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

Ditiny Upload Gateway

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

Deferred Processing Expectations

File TypeThumbnailsAttributes
Raster imagesm, md, lgDeferred queue
SVGNot guaranteedNot guaranteed
Embroiderysm, md, lgDeferred queue
PDFsm, md, lgDeferred queue
Office (DOC/XLS/PPT)sm, md, lgN/A
VideoN/ADeferred queue; attributes may be empty
OtherN/AN/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.

Embroidery Metadata

Processed Wilcom EMB response

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

Dimension semantics

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.

SourceMeaning
wilcom_propsDimensions stored in the Wilcom EMB metadata.
thumbnail_estApproximate fallback when the Wilcom properties are absent.
key omittedDST/PES/JEF and similar formats use the stitch bounding box.
All embroidery keys are optional and source-format dependent. Ditiny can also return 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.

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 example / excerpt

{
  "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. 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.

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

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

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

Update file (name, visibility)

Request example / body

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

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

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 example / excerpt

{
  "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/tagsAdmin Bearer / API Key

Create a tag. The authenticated account must have the admin role.

Request example / body

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

Delete a tag. The authenticated account must have the admin role.

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

Add tag to file

Request example / 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 HTML)

Browser-facing HTML preview page. No API authentication is required, but the file must be public and not deleted. Server-to-server clients that need JSON must use the /json route.

GET/s/{share_token}/jsonNone (share token)

Public JSON metadata for server-to-server clients. Available only when the file is public and not deleted. preview_url, when present, is a temporary presigned URL.

Response example / excerpt

{
  "file_key": "uuid",
  "original_name": "photo.jpg",
  "mime_type": "image/jpeg",
  "file_category": "image",
  "size_bytes": 1048576,
  "file_attributes": {},
  "preview_url": "https://temporary-presigned-url.example",
  "thumbnails": [
    { "size_name": "sm", "status": "pending", "url": null, "width_px": 300, "height_px": 300 },
    { "size_name": "md", "status": "ready", "url": "https://i.ditiny.com/s/{share_token}/thumb/md", "width_px": 600, "height_px": 600 }
  ],
  "is_downloadable": true
}

Thumbnails

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

Webhooks

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. 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"
}
POSTUser webhook URLHMAC / optional Bearer

Ditiny 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 }
  ]
}
GET/api/files/{file_key}/webhook-deliveriesBearer / API Key

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

Rate Limits

EndpointLimit
POST /api/upload600 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, /complete120 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}, /json600 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.

Common Errors

StatusMeaning and action
400 / 422Invalid request or validation error; correct the request before retrying.
401Missing or invalid JWT/API key.
403Inactive account, insufficient role, private share, or API-key domain mismatch.
404 / 410Resource not found, thumbnail not ready, or share file deleted.
409Upload state/object validation conflict; inspect detail before retrying.
413 / 415File is too large or its MIME type is unsupported; do not retry unchanged.
429 / 503Rate 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.

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