Endpoint Reference
Every site-management endpoint, grouped by resource, with examples.
Every endpoint below is scoped to one site, and the site is named by its domain in the URL path. Replace mysite.e9sites.com with your own domain, and put your token in an environment variable called ENINE_TOKEN.
Two groups are deliberately not documented here because they are not part of site management: /api/chat/ drives the built-in chat builder, and /api/contact/ receives submissions from your own site's contact form.
Content
Create, read, update, move, and delete artifacts -- your categories and items.
Every category, section, and content item on your site is an artifact. Categories are roots; items are their children.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/artifacts/ | List roots with their children, paginated |
| POST /api/v1/site/{site}/artifacts/ | Create an artifact, root or child |
| GET /api/v1/site/{site}/artifacts/{slug}/ | Read one artifact with its children |
| POST /api/v1/site/{site}/artifacts/{slug}/ | Update fields, or move it with parent |
| DELETE /api/v1/site/{site}/artifacts/{slug}/ | Delete it |
| PATCH /api/v1/site/{site}/content/save/ | Update named fields on one artifact |
| DELETE /api/v1/site/{site}/content/delete/ | Delete one artifact by id |
| GET /api/v1/site/{site}/artifacts/{slug}/design/ | Read the layout settings of a root |
| POST /api/v1/site/{site}/artifacts/{slug}/design/ | Set section_view, catalog_view, and templates |
Addressing an artifact
The {slug} segment resolves by name first and falls back to the 16-character generated slug, so the name you set is the key you use everywhere. The integer id in responses is what content/save/ and the mapping endpoints need.
Adding an item to a category
Request
curl -X POST \
-H "Authorization: Token $ENINE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "web-design", "title": "Web Design", "parent": "services",
"content": "We build fast, accessible sites."}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/
Send parent or parent_pk to move an artifact later; an empty parent turns it back into a root. A name that is already used returns 409.
Changing one field
Request
curl -X PATCH \
-H "Authorization: Token $ENINE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": "artifact", "pk": 2890, "fields": {"title": "Web and Product Design"}}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/content/save/
The url field in a response is read-only: it is the resolved address of whatever the artifact links to. To change where an item points, create a catalogue entry and set link -- see Links.
Mapping
Link two related items so each appears on the other's page.
A map is a cross-reference between two items -- a project and the service it used, an article and the person who wrote it. Each one appears on the other's page.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/artifacts/{slug}/maps/ | List this artifact's maps, paginated |
| POST /api/v1/site/{site}/artifacts/{slug}/maps/ | Create a map. artifact_b is required |
| GET /api/v1/site/{site}/artifacts/{slug}/maps/{pk}/ | Read one map |
| POST /api/v1/site/{site}/artifacts/{slug}/maps/{pk}/ | Change its target or order |
| DELETE /api/v1/site/{site}/artifacts/{slug}/maps/{pk}/ | Remove the cross-reference |
artifact_b takes the integer id of the other artifact, not its name. Read it from a list or get call first.
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"artifact_b": 3104, "order": 1}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/harbour-rebuild/maps/
import os
import requests
headers = {"Authorization": f"Token {os.environ['ENINE_TOKEN']}"}
base = "https://e9sites.com/api/v1/site/mysite.e9sites.com"
service = requests.get(f"{base}/artifacts/web-design/", headers=headers).json()
requests.post(
f"{base}/artifacts/harbour-rebuild/maps/",
json={"artifact_b": service["id"]},
headers=headers,
)
Both sides have to be items inside a category. Category roots themselves cannot be mapped -- attempting it is rejected.
Media
Upload files, then reference them by their integer id.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/media/ | List media, paginated |
| POST /api/v1/site/{site}/media/ | Upload one or more files |
| GET /api/v1/site/{site}/media/{slug}/ | Read one media row |
| POST /api/v1/site/{site}/media/{slug}/ | Rename it |
| DELETE /api/v1/site/{site}/media/{slug}/ | Delete it |
Uploading is multipart/form-data on the field file, up to 10 MB per file. You can send several in one call.
Request
curl -X POST \
-H "Authorization: Token $ENINE_TOKEN" \
-F "file=@hero.png" \
https://e9sites.com/api/v1/site/mysite.e9sites.com/media/
import os
import requests
with open("hero.png", "rb") as handle:
response = requests.post(
"https://e9sites.com/api/v1/site/mysite.e9sites.com/media/",
files={"file": handle},
headers={"Authorization": f"Token {os.environ['ENINE_TOKEN']}"},
)
print(response.json())
Response
{
"id": 42,
"slug": "hero-png-a1b2c3",
"filename": "hero.png",
"file_type": "png",
"is_image": true,
"url": "https://.../hero.png",
"filehash": "..."
}
Keep the id. Everywhere a file is referenced in a write, it is the integer id:
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"media": 42}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/web-design/
The same is true of logo, favicon, and hero_image inside a configure call. The slug is only for addressing the media row itself.
SEO and AEO
Edit page metadata, set AEO roles, read your JSON-LD, and run audits.
SEO pages
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/seo/pages/ | List SEO pages, paginated |
| POST /api/v1/site/{site}/seo/pages/ | Create one -- canonical, page_name, page_title, description, keywords |
| GET /api/v1/site/{site}/seo/pages/{pk}/ | Read one |
| POST /api/v1/site/{site}/seo/pages/{pk}/ | Update it |
| DELETE /api/v1/site/{site}/seo/pages/{pk}/ | Delete it |
A duplicate canonical returns 409.
AEO data and roles
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/artifacts/{slug}/aeo/ | Read AEO data and FAQs for an artifact |
| POST /api/v1/site/{site}/artifacts/{slug}/aeo/ | Create or replace it |
| PATCH /api/v1/site/{site}/artifacts/{slug}/aeo/ | Change some fields |
| DELETE /api/v1/site/{site}/artifacts/{slug}/aeo/ | Remove it |
| GET /api/v1/site/{site}/artifacts/{slug}/roles/ | List the artifact's roles |
| PUT /api/v1/site/{site}/artifacts/{slug}/roles/ | Replace the whole role set. An empty list clears it |
| POST /api/v1/site/{site}/artifacts/{slug}/roles/ | Add one role |
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"role": "what"}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/web-design/roles/
Reading what you publish
GET /api/v1/site/{site}/aeo/ returns the JSON-LD for every URL in the site's sitemap -- exactly what an answer engine reads. GET /api/v1/manager/inspect/aeo/?url=... does the same for a single page.
Audits
| Endpoint | What it does |
|---|---|
| POST /api/v1/manager/{site}/xeo-audit/ | Run the audit now |
| GET /api/v1/manager/{site}/xeo-check/ | Read the last cached result |
The audit crawls every published page in your sitemap while you wait, so it is slow by design. Run it after a batch of changes rather than after every edit, and read the cached result in between.
Sites
Create sites, read and update configuration, list and import themes.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/ | List every site you can reach |
| POST /api/v1/site/ | Create a site. domain is required |
| GET /api/v1/site/{site}/ | Export the site. Add ?dl=json, yaml, csv, zip, or media |
| POST /api/v1/site/{site}/ | Upsert content into the site |
| GET /api/v1/site/{site}/configure/ | Read config, theme, colours, design, and style |
| POST /api/v1/site/{site}/configure/ | Update any of those |
| GET /api/v1/site/{site}/themes/ | List themes this site can select |
| GET /api/v1/site/{site}/custom-themes/ | Export a custom theme. Requires ?name= |
| POST /api/v1/site/{site}/custom-themes/ | Import or update a custom theme |
| POST /api/v1/site/{site}/random-subdomain/ | Give the site a new random subdomain |
Creating a site
Request
curl -X POST \
-H "Authorization: Token $ENINE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"domain": "mysite.e9sites.com", "name": "My Site"}' \
https://e9sites.com/api/v1/site/
import os
import requests
payload = {"domain": "mysite.e9sites.com", "name": "My Site"}
response = requests.post(
"https://e9sites.com/api/v1/site/",
json=payload,
headers={"Authorization": f"Token {os.environ['ENINE_TOKEN']}"},
)
print(response.status_code, response.json())
Response
{"domain": "mysite.e9sites.com"}
You get 201 on success and 409 if the domain is already taken.
The API gives you exactly what you asked for and nothing more: no sample content, no default hero image, no generated SEO pages. Those conveniences belong to the manager. If you want them here, ask for them explicitly.
Changing configuration
Request
curl -X POST \
-H "Authorization: Token $ENINE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"theme": "print", "config": {"title": "My Site", "subtitle": "Built with the API"}}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/configure/
Media fields inside config take the integer id of a media row -- see Media.
Tags
Create site-wide tags, then assign them to items.
Tags belong to the site; assignments belong to artifacts. Create the tag once, then assign it wherever it applies.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/tags/ | List the site's tags, paginated |
| POST /api/v1/site/{site}/tags/ | Create a tag. name is required |
| GET /api/v1/site/{site}/tags/{tag_slug}/ | Read one tag |
| POST /api/v1/site/{site}/tags/{tag_slug}/ | Rename it |
| DELETE /api/v1/site/{site}/tags/{tag_slug}/ | Delete it everywhere |
| GET /api/v1/site/{site}/artifacts/{slug}/tags/ | List the tags on one artifact |
| POST /api/v1/site/{site}/artifacts/{slug}/tags/ | Assign a tag. tag is required |
| DELETE /api/v1/site/{site}/artifacts/{slug}/tags/{tag_slug}/ | Remove it from that artifact |
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Case Study"}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/tags/
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"tag": "case-study"}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/web-design/tags/
import os
import requests
headers = {"Authorization": f"Token {os.environ['ENINE_TOKEN']}"}
base = "https://e9sites.com/api/v1/site/mysite.e9sites.com"
tag = requests.post(f"{base}/tags/", json={"name": "Case Study"}, headers=headers).json()
requests.post(f"{base}/artifacts/web-design/tags/", json={"tag": tag["slug"]}, headers=headers)
Creating a tag whose name already exists returns 409. Assigning the same tag twice is safe -- you get the existing assignment back rather than an error.
Transfer
Round-trip a whole site as JSON or YAML, and script your backups.
These are the API equivalents of the Export menu in the manager, which makes them the simplest way to back up a site on a schedule.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/ | Export. ?dl=json, yaml, csv, zip, or media |
| POST /api/v1/site/{site}/ | Upsert content into an existing site |
| GET /api/v1/manager/sites/{site}/export/ | Export as a downloadable file. ?output=json or yaml |
| POST /api/v1/manager/sites/{site}/export/ | Upsert content into an existing site |
A nightly backup is one call:
Request
curl -H "Authorization: Token $ENINE_TOKEN" \
"https://e9sites.com/api/v1/site/mysite.e9sites.com/?dl=json" \
-o "mysite-$(date +%F).json"
Pushing content back:
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d @mysite.json \
https://e9sites.com/api/v1/site/mysite.e9sites.com/
import json
import os
import requests
headers = {"Authorization": f"Token {os.environ['ENINE_TOKEN']}"}
base = "https://e9sites.com/api/v1/site/mysite.e9sites.com"
site = requests.get(f"{base}/?dl=json", headers=headers).json()
with open("mysite.json", "w", encoding="utf-8") as handle:
json.dump(site, handle, indent=2)
Two things to know. The format is chosen by the dl or output query parameter, never by an Accept header. And these endpoints are the exception to the usual response shape: they wrap their payload in success and data, where the rest of the API returns the resource directly.
Links
Outbound destinations live in a catalogue; artifacts point at entries by id.
There is no writable url field on an artifact. Every outbound destination -- a link on a card, a social profile, a phone number, an email -- is a catalogue entry that artifacts and the site config point at.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/urls/ | List catalogue entries, paginated |
| POST /api/v1/site/{site}/urls/ | Create an entry. url is required; label and params optional |
| GET /api/v1/site/{site}/urls/{name}/ | Read one entry |
| POST /api/v1/site/{site}/urls/{name}/ | Update it |
| DELETE /api/v1/site/{site}/urls/{name}/ | Delete it |
Three steps: check whether the entry exists, create it if not, then point something at its id.
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"url": "https://www.linkedin.com/company/enine", "label": "LinkedIn"}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/urls/
Response
{"id": 7, "name": "linkedin", "label": "LinkedIn", "url": "https://www.linkedin.com/company/enine", "href": "..."}
Point an item at it:
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"link": 7}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/web-design/
Or use it as a social icon in the footer:
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"config": {"social_links": [7, 8]}}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/configure/
Creating an entry for a URL already in the catalogue returns 409, so list first and reuse the id you find.
Team
List the team and change roles. Admin only, existing accounts only.
| Endpoint | What it does |
|---|---|
| GET /api/v1/site/{site}/users/ | List the site's team, paginated |
| POST /api/v1/site/{site}/users/ | Add an existing account, or change a role. Admin only |
| GET /api/v1/site/{site}/users/{username}/ | Read one member |
| POST /api/v1/site/{site}/users/{username}/ | Change that member's role. Admin only |
Roles are reader, editor, and admin, and both username and role are required when adding someone.
Request
curl -X POST -H "Authorization: Token $ENINE_TOKEN" -H "Content-Type: application/json" \
-d '{"username": "priya", "role": "editor"}' \
https://e9sites.com/api/v1/site/mysite.e9sites.com/users/
Response
{"username": "priya", "email": "priya@example.com", "role": "editor", "job_title": ""}
These endpoints add accounts that already exist. Inviting somebody new by email address happens in the manager, not here. A username nobody holds returns 400 with the field named in errors, not 404 -- the site you addressed does exist, the value inside your payload is what is wrong.