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 needs membership, writing needs editor, team changes need admin.
Reading requires membership of the site. Writing requires the editor role. A few operations require admin.
| Role | Read | Write | Manage the team |
|---|---|---|---|
| reader | yes | no | no |
| editor | yes | yes | no |
| admin | yes | yes | yes |
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
JSON in, JSON out, query parameters for everything else.
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
| Method | Meaning here |
|---|---|
| GET | Read. Site membership is enough. |
| POST | Create, or update an existing resource. Editor role required. |
| PATCH | Partial update -- send only the fields you are changing. |
| PUT | Wholesale replace. Used by artifact roles. |
| DELETE | Remove. 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
One error shape everywhere -- switch on code, not on the message.
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
| Code | What it means here |
|---|---|
| 200 | Read, or a successful update |
| 201 | Created |
| 204 | Deleted, no body returned |
| 400 | The body failed validation, or a value in it points at nothing |
| 401 | Token missing or invalid |
| 403 | Token valid, role insufficient |
| 404 | What you addressed does not exist, including a domain that is not a site |
| 409 | Already taken: site domain, tag name, artifact name, SEO canonical, or catalog URL |
| 429 | Too many requests -- slow down and try again |
| 500 | A 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
List your sites, then read one site's content, in two calls.
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
Request a token, send it in the Authorization header, and keep it secret.
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.
| Method | What you send | Who uses it |
|---|---|---|
| Token | Authorization: Token | Scripts, servers, anything headless |
| Session | The sessionid cookie | The manager in your browser, automatically |
| OAuth2 | Authorization: Bearer | MCP 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.