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

Every category, section, and content item on your site is an artifact. Categories are roots; items are their children.

EndpointWhat 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

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.

EndpointWhat 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

EndpointWhat 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

SEO pages
EndpointWhat 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
EndpointWhat 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
EndpointWhat 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

EndpointWhat 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

Tags belong to the site; assignments belong to artifacts. Create the tag once, then assign it wherever it applies.

EndpointWhat 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

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.

EndpointWhat 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

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.

EndpointWhat 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

EndpointWhat 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.