Skip to main content

Sites

List the sites in your organization, read their details, and create new ones. A site's content is read-only over the API, and so are renaming and deleting — those stay console actions — but you can publish a site, which is what makes writes you made elsewhere in the API appear on the live pages.

Their form submissions are a resource of their own, and that one is not read-only.

The site object​

{
"id": "host_demo",
"object": "site",
"displayName": "Demo Bakery",
"subdomain": "demo",
"domain": null
}
FieldTypeNotes
idstringSite id — use it in the paths below.
objectstringAlways "site".
displayNamestring | nullName shown in the console.
subdomainstring | nullThe {subdomain}.aglyn.app address.
domainstring | nullThe custom domain, once verified — null if the site only uses its subdomain.

The form submission object​

{
"id": "sub_1",
"object": "form_submission",
"form": "contact",
"path": "/contact",
"fields": { "email": "hi@example.com", "message": "Hello!" },
"read": false,
"created": "2026-07-20T18:23:23.950Z"
}
FieldTypeNotes
formstring | nullThe form's name — what you filter on.
pathstring | nullThe page path it was submitted from.
fieldsobjectSubmitted values, keyed by field name. Shape follows the form's design, so it varies per form.
readbooleanWhether it's been marked read in the console inbox. Reading over the API doesn't change it.
createdstring | nullISO 8601.

Endpoints​

List sites​

GET /v1/sites — scope sites:read. Paginated, ordered by site id.

curl "https://app.aglyn.com/api/v1/sites" \
-H "Authorization: Bearer aglyn_sk_…"

Create a site​

POST /v1/sites — scope sites:write. Accepts an Idempotency-Key.

curl -X POST "https://app.aglyn.com/api/v1/sites" \
-H "Authorization: Bearer aglyn_sk_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "displayName": "Demo Bakery", "subdomain": "demo-bakery" }'
FieldRequiredNotes
displayNameyesName shown in the console. Trimmed, truncated at 80 characters.
subdomainyes3–30 characters: lowercase letters, numbers and hyphens, starting with a letter or number. Becomes {subdomain}.aglyn.app.

Returns 201 with the site object. A replayed request returns the same site with 200 — so if you retry after a dropped connection, compare the status to tell a fresh create from a replay.

Send an Idempotency-Key. Subdomains are globally unique, so a retry that reuses the same one happens to 409 — but that is an accident of the namespace, not a guarantee. If your client generates a subdomain per attempt, a retry without a key creates a second site, which consumes a slot of your site allowance.

Refusals:

StatuscodeMeans
400validation_failedMissing displayName, or a subdomain that is malformed or reserved. Never consumes the idempotency key — fix the body and retry with the same one.
403site_quotaYour plan's site limit is reached. Upgrade or add extra sites, then retry with the same key.
409subdomain_takenSomething already uses that subdomain. The message lists free alternatives. Also the answer when another request claims the same subdomain at the same moment: uniqueness is settled inside the transaction that creates the site, so exactly one of two simultaneous creates wins and the loser writes nothing at all — no site, no slot spent.
429—More than 10 creates in an hour for this organization. Honor Retry-After.

Each of those releases the key, so the retry that should finally succeed is not answered with a replay of the refusal.

Renaming and deleting a site are deliberately not available over the API. A delete would erase every page, version, product and uploaded file immediately, with no hold and no undo, from one field in a request body — that is not something to expose to an automated caller before a soft-delete exists.

Retrieve a site​

GET /v1/sites/{siteId} — scope sites:read.

A site your organization doesn't own returns 404 not_found ("No such site") rather than 403 — the API doesn't reveal whether an id exists elsewhere. So a 404 here means "not yours or not real", not "your key is missing a scope".

Publish​

POST /v1/sites/{siteId}/publish — scope sites:publish.

Refreshes the site's live pages so data you wrote over the API appears now instead of when the cache happens to expire.

You need this more often than it first looks. Writing a dataset record changes what a page would render, but a live page is cached: pages are rebuilt at most once an hour, the dataset reads behind them are cached for an hour as well, and the page cache is stale-while-revalidate — the first visitor after the window still gets the old copy while the new one is built behind them. So without a publish, "my record is in the API but not on the site" is the expected behavior for an hour or more, and you cannot tell it apart from a write that failed.

curl -X POST "https://app.aglyn.com/api/v1/sites/host_demo/publish" \
-H "Authorization: Bearer aglyn_sk_…"
{
"object": "publish",
"site": "host_demo",
"published": true,
"reason": null,
"pages": 12,
"pagesDropped": 0
}
FieldNotes
publishedtrue when the site's pages were refreshed. Check it — see below.
reasonnull on success. Otherwise why not: "not_routed" (the site has no live pages yet), "not-configured", "tenant-{status}", "error".
pagesHow many cached pages were dropped.
pagesDroppedPages not refreshed because the site exceeded the 250-page limit for one call. They catch up on their own when their cached copy expires, within the hour.

A 200 does not always mean published. published: false with a reason is the honest answer for a site with nothing routed yet, or a refresh we could not complete — reported rather than hidden, because otherwise you would poll a page that is never going to change. Treat published as the field that matters, not the status code.

Publishing is idempotent and takes no Idempotency-Key: publishing twice lands the same state and returns the same answer.

It is rate limited per site, not per key​

10 publishes per site per hour, counted separately from the 120 requests a minute your key gets. One publish can drop up to 250 pages and each one costs real work to rebuild, so this limit is sized to the work rather than to the request — and minting more keys does not raise it, because the budget belongs to the site.

Over budget returns 429 rate_limited with a Retry-After. Nothing is lost when it does: the hour-long cache is still underneath, so the change appears on its own — an hour or more later rather than now.

Publish once at the end of a batch, not after every record. A sync that writes 500 records and publishes once is both faster and within budget; one that publishes per record is refused after ten and gains nothing over waiting.

Form submissions​

Moved to their own page: Form submissions. They live under a site (/v1/sites/{siteId}/form-submissions) but carry their own scopes — forms:read to read, forms:write to mark read or delete — and, unlike a site, they can be written.

Errors​

StatustypeWhen
403insufficient_scopeKey lacks sites:read, or sites:publish on the publish path.
404not_foundUnknown or unowned site; unknown sub-path.
405method_not_allowedAnything other than GET, or anything other than POST on /publish.
429rate_limitedThe site's publish budget is spent. Carries Retry-After.