API Basics

Tokens, base URL, request conventions, errors, and permissions.


API Basics covers what every call needs: a token, the right header, the shape of requests and responses, what an error looks like, and which role the call requires.

Permissions

Reading requires membership of the site. Writing requires the editor role. A few operations require admin.

RoleReadWriteManage the team
readeryesnono
editoryesyesno
adminyesyesyes

Every unsafe method -- POST, PATCH, PUT, DELETE -- is editor-gated automatically, so a reader token can explore the whole API without any risk of changing something.

Admin only
  • Adding a team member to a site, or changing someone's role.

A 403 means the endpoint worked and your role did not. Check which role you hold on that site before assuming the call is wrong.

Conventions

Content type

Requests and responses are JSON, with two exceptions: media uploads are multipart/form-data, and the public contact endpoint is form-encoded. The API never picks a format from your Accept header. When an endpoint can return something other than JSON, you ask with a query parameter such as ?dl=yaml.

What the methods mean
MethodMeaning here
GETRead. Site membership is enough.
POSTCreate, or update an existing resource. Editor role required.
PATCHPartial update -- send only the fields you are changing.
PUTWholesale replace. Used by artifact roles.
DELETERemove. Returns 204 with no body.

Send the whole object on POST when you mean to replace a resource, and use PATCH when you only want to change a few fields.

Pagination

List endpoints return 50 items per page. Use ?page= and ?page_size=, up to a maximum of 200.

Request
curl -H "Authorization: Token $ENINE_TOKEN" \
  "https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/?page=2&page_size=25"
Response
{
  "count": 137,
  "next": "https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/?page=3&page_size=25",
  "previous": "https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/?page=1&page_size=25",
  "results": []
}

Not every list is paginated. The theme list, an artifact's tags, and the site AEO surface always return a bare array, and ?page_size=0 switches pagination off. Write your client to accept both a bare array and a count-and-results envelope.

Repeating a call

Assigning a tag or adding a role twice is safe -- those endpoints return the existing row instead of failing. There is no Idempotency-Key header.

Request volume

Batch your calls where you can, and avoid tight polling loops. If you need to move a lot of content at once, the export and import endpoints do it in a single call.

Errors

Every error uses the same shape -- RFC 9457 Problem Details -- and the content type application/problem+json.

Response
{
  "type": "https://api.eninesites.com/errors/permission_denied",
  "title": "Forbidden",
  "status": 403,
  "code": "permission_denied",
  "detail": "Editor or admin role required on 'mysite.e9sites.com'.",
  "instance": "/api/v1/site/mysite.e9sites.com/artifacts/"
}

Switch on code. It is a stable machine-readable string. detail is written for people and may be reworded at any time.

A validation failure adds an errors object, one entry per offending field:

Response
{
  "type": "https://api.eninesites.com/errors/validation_error",
  "title": "Bad Request",
  "status": 400,
  "code": "validation_error",
  "detail": "Request body failed validation.",
  "errors": {"username": ["User not found."]}
}
Status codes
CodeWhat it means here
200Read, or a successful update
201Created
204Deleted, no body returned
400The body failed validation, or a value in it points at nothing
401Token missing or invalid
403Token valid, role insufficient
404What you addressed does not exist, including a domain that is not a site
409Already taken: site domain, tag name, artifact name, SEO canonical, or catalog URL
429Too many requests -- slow down and try again
500A server-side precondition failed -- surface it rather than retrying

The split between 400 and 404 is deliberate. Addressing something that does not exist gives 404. A value inside your payload pointing at nothing gives 400, with the field named in errors.

First Request

Two calls are enough to confirm your token works before you write anything larger.

Step 1: List your sites

GET /api/v1/site/ returns every site your account can reach.

Request
curl -H "Authorization: Token $ENINE_TOKEN" \
  https://e9sites.com/api/v1/site/
import os
import requests
headers = {"Authorization": f"Token {os.environ['ENINE_TOKEN']}"}
response = requests.get("https://e9sites.com/api/v1/site/", headers=headers)
print(response.json())
Response
[
  {"domain": "mysite.e9sites.com", "name": "My Site"}
]
Step 2: Read that site's content

Nearly every other endpoint is scoped to one site, named by its domain in the path.

Request
curl -H "Authorization: Token $ENINE_TOKEN" \
  https://e9sites.com/api/v1/site/mysite.e9sites.com/artifacts/

The domain has to match the site's domain exactly -- there are no aliases, and a domain that is not a site returns 404.

If something goes wrong
  • 401 -- the token is missing, malformed, or deactivated. Check the header spells Token, not Bearer.
  • 403 -- the token is fine, but your role on that site is not high enough for what you asked.
  • 404 -- the domain in the path is not a site you can see.

Tokens

Every API call must be authenticated. An anonymous request returns 401.

There are three ways to authenticate, and the right one depends on how you are calling.

MethodWhat you sendWho uses it
TokenAuthorization: TokenScripts, servers, anything headless
SessionThe sessionid cookieThe manager in your browser, automatically
OAuth2Authorization: BearerMCP clients, such as an AI assistant
Getting a token

Tokens are issued by us rather than self-serve. Email hello@in.eninesites.com from the address on your account and say which site the token is for. You will get back a single string.

Using it
Request
curl -H "Authorization: Token $ENINE_TOKEN" \
  https://e9sites.com/api/v1/site/
import os
import requests
headers = {"Authorization": f"Token {os.environ['ENINE_TOKEN']}"}
response = requests.get("https://e9sites.com/api/v1/site/", headers=headers)
response.raise_for_status()
print(response.json())
Keeping it safe
  • Treat the token like a password. It carries your role on every site you belong to.
  • Keep it in an environment variable, never in code you commit.
  • Tokens do not expire on a schedule -- they stay valid until deactivated.
  • If one leaks, email us and we will deactivate it immediately.