Documentation
Dynjandi stores your images and serves resized, cropped and re-encoded variants of them from cdn.dynjandi.dev. This page covers the URLs, uploading, API keys, usage and the MCP server.
Reference documents:
- The management API, as an OpenAPI 3.1 document. It lists the routes an API key can reach.
- The public-key API, as an OpenAPI document in the public repository.
@dynjandi/transform-grammaron npm: the URL grammar, which parses and builds variant URLs.@dynjandi/sdkon npm: a TypeScript SDK for Dynjandi.
URLs
A variant URL is the file id followed by operations. Each operation starts with /-/, and operations apply in order. The file id alone, https://cdn.dynjandi.dev/{id}, is the original.
https://cdn.dynjandi.dev/{id}/-/resize/800x/-/format/webp/-/quality/75/
https://cdn.dynjandi.dev/{id}/-/crop/400x400/center/
https://cdn.dynjandi.dev/{id}/-/scale_crop/1280x400/focal/0.42x0.18/
https://cdn.dynjandi.dev/{id}/-/scale_crop/1280x400/face,attention/
Operations
| Operation | Form | Notes |
|---|---|---|
resize | resize/{w}x{h}, resize/{w}x or resize/x{h} | Width, height or both. At least one is required. |
crop | crop/{w}x{h}/{position} | The position is required. |
scale_crop | scale_crop/{w}x{h}/{position} | The position is optional and defaults to center. |
format | format/{format} | jpg is accepted as jpeg. |
quality | quality/{n} | An integer, or a keyword: lightest is 55, lighter 70, normal and smart 80, better 88 and best 95. |
Limits and values
| What | Allowed |
|---|---|
| Width and height | 1 to 10,000 pixels |
| Quality | 1 to 100 |
| Focal coordinates | 0 to 1 |
| Formats | jpeg, png, webp, avif |
| Positions | center, north, south, east, west, northeast, northwest, southeast, southwest, attention, entropy |
Positions
- A position names where a crop is anchored.
focal/{x}x{y}anchors it on a point, with each coordinate a fraction between 0 and 1. faceruns face detection. When no face is found the crop falls back to the next position, soface,attentiontries a face and thenattention. A barefacemeansface,center.- A fallback chain is
facefollowed by one position from the table above.focalcannot be part of a chain.
Uploadcare URLs
Uploadcare spellings are accepted and normalised to the canonical form, so both spellings share one cache entry.
| Uploadcare | Canonical |
|---|---|
scale_crop/650x300/smart | crop/650x300/face,center |
quality/smart | quality/80 |
a trailing /image.jpeg | ignored |
empty path segments (//) | ignored |
Caching
A variant is rendered on its first request and served from the cache after that. Variants are served with Cache-Control: public, max-age=31536000, immutable. A URL the grammar rejects answers 400.
Upload
There are two ways to upload. Both take a multipart/form-data body with a file part and, optionally, focalX and focalY parts. The focal point is set with both fields or neither.
With a public key
POST https://cdn.dynjandi.dev/upload
X-Public-Key: {public key}
Content-Type: multipart/form-data
The public key is the only credential this route checks, so send it from your server and not from a page. A successful upload answers 201 with file, id, url, focalX and focalY. The response is Cache-Control: no-store.
This route reports an error as { "error": "message" }, with a string message.
| Status | When |
|---|---|
400 | The body is not multipart or cannot be read, there is no file or it is empty, or the focal fields are incomplete or out of range. |
401 | The X-Public-Key header is missing or the key is not valid. |
402 | The workspace does not allow uploads, or the upload would pass its storage cap. |
413 | The file is larger than the project's maximum file size, or larger than its remaining storage quota. |
With an API key
POST https://app.dynjandi.dev/api/projects/{id}/files
Authorization: Bearer dyn_...
Content-Type: multipart/form-data
This route reports an error as a JSON body such as { "error": { "code": "NOT_FOUND", "message": "…" } }, where code and message are strings and an optional details object may sit beside them. The statuses are 400, 401, 403, 404 and 429, and, for this route, 402, 413 and 502. A 429 carries Retry-After.
API keys
An API key lets a server call the management API without a browser session.
- A member creates a key for their own workspace in the dashboard, on the API keys page at
app.dynjandi.dev/api-keys. A key cannot reach the routes that create, list or revoke keys, so a leaked key cannot mint its own replacement. - A key expires in 30, 90 or 365 days, and 90 is the default. A key never lasts without an expiry.
- A workspace holds at most 10 unexpired keys. The name is 1 to 64 characters.
- A key starts with
dyn_. The key itself is shown once, when it is created, and cannot be read back afterwards. A member can revoke a key from the dashboard. - A key acts as a member of the one workspace it was created for, and never as a superadmin.
Sending a key
GET https://app.dynjandi.dev/api/workspace
Authorization: Bearer dyn_...
- The key goes in the
Authorization: Bearerheader. When that header is present the session cookie is never read, so an invalid key does not fall back to a cookie. - A key is meant for servers: the management API is called by servers with a key, and it has no CORS.
- Treat a key like a password. Keep it out of URLs and out of version control.
When a request is refused
| Status | When |
|---|---|
401 | The key is expired, revoked or malformed, or its owner is disabled, is a superadmin, is no longer a member of the workspace, or is gone. Every one of these answers the same 401. |
404 | The route is not on the list below. A route a key cannot reach answers 404, not 403. Another workspace's project or file also answers 404. |
429 | A key is limited to 120 requests a minute, and a client address to 10 failed attempts a minute. A 429 carries Retry-After: 60. |
Routes a key can reach
Only these routes accept a key. Every route in the table also accepts HEAD, except /api/files/{id}/focal-suggestion, which is GET only.
| Route | Methods |
|---|---|
/api/workspace | GET |
/api/workspace/usage | GET |
/api/projects | GET, POST |
/api/projects/{id} | GET, PATCH |
/api/projects/{id}/files | GET, POST |
/api/files/{id} | GET, PATCH, DELETE |
/api/files/{id}/focal-suggestion | GET |
Usage
GET /api/workspace/usage reports on the one workspace the key was created for. GET /api/workspace returns the same body. A key names no workspace in the request, because it has exactly one. The body holds:
id,name,status,plan,billingStatusandcancelAtPeriodEnd;periodStartandperiodEnd;caps, withstorageBytes,variantsandprojects, wherenullmeans uncapped; andusage, withstorageBytes,newVariantsandprojects.
It does not report delivery bandwidth. Every response is Cache-Control: no-store, and errors use the { "error": { "code": "…", "message": "…" } } body of every management API route.
GET https://app.dynjandi.dev/api/workspace/usage
Authorization: Bearer dyn_...
MCP server
An agent can manage a workspace, build and check variant URLs and read these docs through a remote MCP server. It uses the same API key as the management API.
https://mcp.dynjandi.dev/mcp
Connecting
- The server speaks MCP over Streamable HTTP, and every request is a
POSTto/mcp. - Every request carries
Authorization: Bearerfollowed by an API key, set once in the client's configuration. There is no sign-in flow and no OAuth. - The server is for MCP clients that run on a server or on your own machine. It answers any request that carries an
Originheader with403and has no CORS, so a web page cannot call it. - A request with no bearer header, or one that is not 1 to 256 characters without spaces, answers
401withWWW-Authenticate: Bearer realm="dynjandi". A client address is limited to 120 requests a minute, and a429carriesRetry-After: 60.
The field names differ between clients. The URL and the header are what they all need:
{
"mcpServers": {
"dynjandi": {
"type": "http",
"url": "https://mcp.dynjandi.dev/mcp",
"headers": {
"Authorization": "Bearer dyn_..."
}
}
}
}
Keep the configuration file out of version control, as for any other key.
Tools
There are twelve tools. The first nine act on the one workspace the key was created for, and the dashboard checks the key on each of those calls. The last three need no key that is real, only a well-formed bearer, and they never call the dashboard.
| Tool | What it does |
|---|---|
get_workspace | Returns the workspace: its plan, status, caps and usage. |
list_projects | Lists the workspace's projects. |
get_project | Returns one project with its file count and bytes used. |
create_project | Creates a project. The plan may refuse it, for a cap or an inactive plan. |
rename_project | Renames a project. The old name is overwritten. |
list_files | Lists one page of a project's files, 24 to a page. |
get_file | Returns one file's metadata, including its focal point. |
set_focal_point | Sets a file's focal point, replacing the old one, or clears it with null for both coordinates. |
suggest_focal_point | Asks the image service where the file's subject is. Nothing is saved, and each call costs container time. |
build_variant_url | Builds a variant URL from a file id and a list of operations, and checks the operations against the limits above. |
parse_variant_url | Parses a variant URL, or just its path, into its operations and its canonical URL. |
read_docs | Returns one section of these docs by heading, or the list of headings. |
A tool failure comes back as an error result with an error code and a message, not as an HTTP status. A key the dashboard refuses is one such result.
What is not a tool
Uploading a file and deleting a file are not tools. Both stay on the REST API: see Upload and API keys. A project's public key is never returned by a tool.