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 screen 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.
429More than 10 creates in an hour for this organization. Honour 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 screen, 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 every 60 seconds, and that 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 behaviour for up to a couple of minutes, 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 within a minute.

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 60-second cache window is still underneath, so the change appears on its own shortly after.

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.