Automation API
The Automation API is the authenticated, language-independent transport for local AI tools, bulk editors, and the PHP-only workspace client. It uses the same canonical website resources and revision history as browser administration. It is not a public frontend API and does not require Node on the DynoStatic host.
This document describes version 1's currently available read, trusted website-starter, atomic change-set, conflict-resolution, allowlisted-command, resumable original-upload, verified original-download, verified replica-policy, and static-delivery-manifest surfaces. The capabilities response never advertises an unfinished action.
Create a token
Open Automation API in the selected website's Data navigation. Website managers can create a token bound to that website. Only the installation owner can create an owner token.
Start with workspace:read. Add a write permission only for a client that actually needs it. The plaintext token is displayed once, while DynoStatic stores only its lookup prefix and HMAC. Keep the token in a protected environment variable or credential store. Never write it into a workspace, browser bundle, public repository, generated website, or URL.
Site tokens always use their bound website. Owner tokens must pass the target website's public ULID as site=... on every website request. Omitting it fails instead of choosing a website implicitly.
Discover the contract
curl -H "Authorization: Bearer $DYNOSTATIC_TOKEN" \
"https://panel.example.com/api/v1/automation/capabilities"
For an owner token:
curl -H "Authorization: Bearer $DYNOSTATIC_TOKEN" \
"https://panel.example.com/api/v1/automation/capabilities?site=SITE_ULID"
The response identifies the selected website, token class and scopes, supported resource types, available actions, and transport limits. A client should discover this response instead of assuming that a planned action is installed.
When the token grants commands:write, commands contains the complete installed command catalog, including each command's purpose, exact argument shape, scope, and retry policy. Tokens without that scope receive an empty command catalog and no command action.
Read endpoints
All endpoints require Authorization: Bearer .... Read endpoints require workspace:read, return Cache-Control: private, no-store, and use signed opaque continuation tokens.
| Endpoint | Required query | Purpose |
|---|---|---|
GET /api/v1/automation/capabilities |
owner site optional |
Discover the exact installed contract |
GET /api/v1/automation/snapshot |
owner site; optional limit, page |
Read every canonical resource at one stable website cursor |
GET /api/v1/automation/delta |
since; owner site; optional limit, page |
Read current resources changed after a prior website cursor |
GET /api/v1/automation/site-starters |
owner site |
Preview the closed trusted starter catalog against the current website |
GET /api/v1/automation/conflicts |
owner site; optional status, limit, page |
List open, resolved, or all persisted conflicts |
GET /api/v1/automation/revisions |
resource_type, resource_id; owner site; optional limit, page |
Read main and parallel revision history for one resource |
limit defaults to 100 and cannot exceed 250. Treat page as opaque and resend the original filter parameters with it. Snapshot and delta pagination returns HTTP 409 if the website changes between pages; restart at page one rather than combining two states.
Snapshot example:
curl -H "Authorization: Bearer $DYNOSTATIC_TOKEN" \
"https://panel.example.com/api/v1/automation/snapshot?limit=100"
Delta example, using the prior response's site_cursor:
curl -H "Authorization: Bearer $DYNOSTATIC_TOKEN" \
"https://panel.example.com/api/v1/automation/delta?since=1234&limit=100"
Initialize a website from a trusted starter
The closed server-bundled catalog contains blog, business, and portfolio. A catalog request requires workspace:read and returns a fresh read-only preview for every starter, including its exact resource classifications, conflicts, and preview_hash. It creates no reservation, change set, audit event, or content:
php bin/dynostatic-workspace site-starters \
--server=https://panel.example.com \
--site=SITE_ULID > starter-previews.json
Choose a preview whose can_apply value is true, then use a token with workspace:write to submit only the closed starter key and its exact preview hash:
php bin/dynostatic-workspace site-starter-apply \
--server=https://panel.example.com \
--site=SITE_ULID \
--starter=business \
--preview-hash=LOWERCASE_SHA256
The equivalent direct request is POST /api/v1/automation/site-starters/apply?site=SITE_ULID with this exact JSON object:
{
"format": "dynostatic.site-starter-apply",
"version": 1,
"starter": "business",
"preview_hash": "LOWERCASE_SHA256"
}
Unknown or missing fields fail closed. The caller cannot submit schemas, entries, IDs, definition hashes, presentation patches, client identities, credentials, executable content, or deletion authority. DynoStatic re-previews the current website and applies the same server-owned transaction used by the administrator screen. A stale or concurrent website change returns HTTP 409 without a partial installation; an exact replay is an inert success that preserves every later edit.
There are three supported workflows:
- Browser-first: apply a starter from Content, then pull the resulting website.
- API-first: preview and apply with the commands above, then run the ordinary
pullcommand. - Mixed editing: continue with normal browser edits or local
pull,site-build,push, and conflict resolution at any time.
The result is only ordinary cms.schema, cms.entry, and site.presentation resources. There is no starter file, projection, package, credential, provider call, deployment, Node process, or hidden replacement authority in the local workspace. Applying a starter publishes content inside DynoStatic but does not deploy a remote website.
Initialize a PHP-only local workspace
For a community website, discover automation.community.setup and follow the community setup guide. It uses the same owner-only Deals/Discussions presets as Boards and requires workspace:write. The installed setup response includes current Comments choices and defaults; Comments are created and published through the existing atomic widget change-set API. These operations do not require a browser session or publish a remote website.
The source distribution and separately built single-file PHAR provide a standalone PHP client that does not load an installed panel configuration, create panel runtime storage, or connect to MySQL. Create an empty directory, provide a workspace:read Automation token through the environment, and run:
mkdir my-site
export DYNOSTATIC_TOKEN='the one-time token'
php bin/dynostatic-workspace pull \
--server=https://panel.example.com \
--site=SITE_ULID \
--workspace=/absolute/path/to/my-site
Substitute php dynostatic-workspace.phar for php bin/dynostatic-workspace in every example when using the packaged client. A release maintainer builds that artifact from an exact clean source commit with PHP alone:
SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" \
php -d phar.readonly=0 bin/build-workspace-phar.php \
--output=/absolute/path/dynostatic-workspace.phar
The builder uses a literal local-client source allowlist, embeds build identity and a SHA-256 payload manifest, applies a SHA-512 PHAR signature, and normalizes entry timestamps so the same commit and epoch produce byte-identical output. The finished PHAR runs with ordinary read-only PHAR settings and needs no Composer or Node installation. Building the artifact is a maintainer action; neither the hosted panel nor an operator running the finished client needs phar.readonly=0.
Use --token-stdin instead of the environment when a credential manager can pipe the token. Token values are deliberately not accepted as command-line arguments because process listings and shell history may expose them. HTTPS is mandatory except for explicit loopback development such as http://127.0.0.1:4321.
Pull the portable site graph
AI, migration, and static-site tooling can inspect the same stable snapshot as one canonical, secret-free graph without creating a workspace directory:
export DYNOSTATIC_TOKEN='the one-time token'
php bin/dynostatic-workspace graph-pull \
--server=https://panel.example.com \
--site=SITE_ULID > site-graph.json
graph-pull requires only workspace:read. It writes JSON to standard output and diagnostics to standard error, never persists the token, and accepts --token-stdin like ordinary pull. The version-one graph contains the complete canonical website resource set, including website policy, tracked webhook endpoints, widgets and their workflow connections, CMS schemas/entries, and media assets/collections, plus closed relationship kinds for entry-to-Blueprint ownership, schema-declared CMS relationships, and declared resource/media dependencies. Every workflow connection has two required payload-derived edges to its source and destination widgets. Required internal targets must exist; optional external/shared references remain explicit. Website policy and webhook endpoints are graph-neutral: neither caller-supplied dependencies nor edges may turn them into relationship authority. Missing, extra, duplicated, authority-downgraded, or invented edges fail closed when decoded.
The graph does not contain media bytes, provider locators, protected paths, credentials, cookies, internal numeric database IDs, or generated frontend assets. Resource and edge order is deterministic. For reproducible fixtures or signed offline bundles, pass an exact UTC timestamp such as --generated-at=2026-08-26T04:00:00Z; otherwise the observation time is generated when the snapshot is assembled.
The first pull refuses to overwrite any pre-existing projected target. It retries pagination when the server reports a changed snapshot, validates duplicate-free page/resource identities and canonical hashes, stores exact content-addressed bases under .dynostatic, and publishes the visible projections plus state through one recoverable local mutation. No token, authorization header, cookie, protected server path, or database credential is written locally.
CMS schemas are combined in blueprint.json. Structured entries remain lossless JSON. Markdown and MDX entries use front matter whose single canonical JSON flow object is valid YAML 1.2, followed by the exact inert document body. HTML and plain-text bodies remain untouched and use a .meta.json sidecar. The client never evaluates MDX, HTML, templates, or scripts.
Running pull again performs a three-way merge from each stored base. A server-only edit replaces the clean local projection; a local-only edit remains as a diff against the newer server base; disjoint object edits merge; ordered lists and overlapping scalar/object edits produce a durable .dynostatic/conflicts/... record while preserving the local proposal in visible files and leaving the server unchanged. Tracked path moves delete only their old owned files. Untracked destination files, concurrent state changes, and manual edits made while the network request is active all defeat the atomic activation rather than being overwritten. Existing conflicts must be resolved before another pull.
Offline inspection requires no token or network access:
php bin/dynostatic-workspace validate --workspace=/absolute/path/to/my-site
php bin/dynostatic-workspace diff --workspace=/absolute/path/to/my-site
validate strictly reconstructs every tracked canonical payload. diff performs the same validation and reports additions, modifications, and explicit deletions. Canonical new widgets, widget connections, Blueprint schemas, CMS entries, and media assets are discovered in their reserved paths. Connections project to workflows/<CONNECTION_ULID>.json; both endpoints must be widgets in the same website. Existing webhook endpoints project to webhooks/<ENDPOINT_ULID>.json and may be edited or explicitly deleted locally, but an untracked file there is rejected: endpoint creation must first bind a new signing secret through the protected administrator workflow. Deletion is always explicit:
php bin/dynostatic-workspace delete \
--workspace=/absolute/path/to/my-site \
--resource-type=cms.entry \
--resource=RESOURCE_ULID
php bin/dynostatic-workspace conflicts --workspace=/absolute/path/to/my-site
php bin/dynostatic-workspace resolve \
--workspace=/absolute/path/to/my-site \
--resource-type=cms.entry \
--resource=RESOURCE_ULID \
--use=local
resolve --use=local|server is an explicit whole-resource choice for a local pull conflict. It does not silently choose a winner. To publish the complete local A/M/D set atomically, use a token with workspace:write and run:
php bin/dynostatic-workspace push \
--server=https://panel.example.com \
--workspace=/absolute/path/to/my-site
When the diff contains tombstones, add --allow-delete; without that acknowledgement, no deletion is sent. Push persists its retry IDs and accepted offsets before network I/O, resumes the same server change set after interruption, publishes all resources or none, verifies the authoritative snapshot, and only then advances local bases and removes retry control. If another writer moved an overlapping head, the server preserves a parallel proposal and the client reports the conflict instead of overwriting it.
Work with media originals locally
Pulled resource JSON contains immutable media facts but not protected bytes. Fetch an exact current original into its canonical content-addressed path with a media:read token:
php bin/dynostatic-workspace media-pull \
--server=https://panel.example.com \
--workspace=/absolute/path/to/my-site \
--resource=ASSET_ULID
php bin/dynostatic-workspace media-status \
--workspace=/absolute/path/to/my-site \
--resource=ASSET_ULID
The download is streamed and must match the pinned asset ID, revision, MIME, byte count, and SHA-256 before it becomes visible. media-remove-local removes only the verified local companion and leaves the server resource and replica untouched. This gives an operator a reversible server-only or local-plus-server workspace without rewriting content references.
Create a site-owned media asset from JPEG, PNG, WebP, PDF, or plain text without hand-authoring hashes or upload envelopes:
php bin/dynostatic-workspace media-add \
--workspace=/absolute/path/to/my-site \
--file=/absolute/path/to/hero.png \
--title='Homepage hero' \
--alt='A mountain at sunrise'
The local command detects and verifies the bytes, creates the stable asset ULID and portable asset.json, and atomically copies the immutable original. An ordinary push then begins or resumes the protected upload, advances in 4 MiB chunks, verifies completion, injects a transient upload binding, and publishes the media resource in the same change-set transaction. The server-authoritative refresh removes that binding and upload ledger. An expired unconsumed upload is safely restarted under a new ID; consumed, cancelled, or verification-failed state remains fail-closed.
Replace an existing website-owned original while retaining its stable asset ULID and metadata:
php bin/dynostatic-workspace media-replace \
--workspace=/absolute/path/to/my-site \
--resource=ASSET_ULID \
--file=/absolute/path/to/new-hero.png \
--title='Updated homepage hero'
The command advances the immutable asset revision, removes only the superseded local companion, and leaves --title/--alt unchanged when those options are omitted. The next push uses the same resumable and atomic publication path as creation. A target with an existing unpushed original, replica mutation, upload, or push is refused instead of being overwritten.
Move replicas and return static delivery
Replica copy and retirement use the exact JSON envelopes documented below. Save an envelope to a local file, pin it to the revision and SHA-256 in the workspace, and run:
php bin/dynostatic-workspace media-replica-copy \
--server=https://panel.example.com --workspace=/absolute/path/to/my-site \
--resource=ASSET_ULID --file=replica.json
php bin/dynostatic-workspace media-replica-retire \
--server=https://panel.example.com --workspace=/absolute/path/to/my-site \
--resource=ASSET_ULID --replica=REPLICA_ULID --file=retire.json
The client refuses stale local facts and validates every public response identity. Version 1 truthfully offers only the configured DynoStatic provider; future provider packages can use the same operation without changing asset IDs. The server will never retire the last verified original. Consequently “local-only custody” is not claimed by the initial provider package: a workspace may own build input and static delivery, but protected server custody remains until another configured verified-original provider can replace it safely.
A static builder returns data-only public variants through the same local client:
php bin/dynostatic-workspace media-delivery-list \
--server=https://pan