REST API
Everything in your portfolio over plain HTTP and JSON: for scripts, spreadsheets, CI jobs, internal dashboards and your own apps. It offers the same operations as the MCP server, with the same token.
1. Create a token
Open the API page in your Lemon Domains account and click New token. Choose Read-only for reports and dashboards, or Read and write to make changes. You see the value once. The same token also connects AI tools over MCP.
2. Check it works
terminalcurl https://lemon.domains/api/v1/me \
-H "Authorization: Bearer YOUR_TOKEN"You get back your account and the token's access level.
3. Ask for your domains
terminalcurl "https://lemon.domains/api/v1/domains/expiring?withinDays=60" \
-H "Authorization: Bearer YOUR_TOKEN"response{
"count": 1,
"withinDays": 60,
"includeExpired": false,
"domains": [
{
"id": "6f1c…",
"domainName": "example.com",
"status": "Active",
"autoRenew": false,
"expiryDate": "2026-12-01",
"displayStatus": "Active",
"projectName": "Acme",
"registrarAccountDisplayName": "Main",
"registrarName": "Porkbun"
}
]
}4. Make a change
terminalcurl -X POST https://lemon.domains/api/v1/domains \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domainName": "coolapp.io",
"projectId": "PROJECT_ID",
"registrarAccountId": "REGISTRAR_ACCOUNT_ID",
"expiryDate": "2027-03-01"
}'Get the ids from GET /projects and GET /registrar-accounts. Changes made through the API show up in each domain's history as API.
Conventions
- Paging.
GET /domainstakeslimit(up to 500) andoffset, and returnstotalandhasMore. - Status.
displayStatusis the status the app shows: Active, Expiring soon, Expired, Unknown, Not at registrar or Archived. It follows the expiry date and registrar sync. - Archived domains. Archive a domain you no longer track with
PATCH /domains/{id}and{"archived": true, "archivedNote": "Moved to the client"}. Lists and counts leave archived domains out; passarchived=includeorarchived=onlytoGET /domainsto see them. - Look up by name.
GET /domains/example.comworks as well asGET /domains/<id>. - Partial updates.
PATCHchanges only the fields you send. Unknown fields are rejected, so a typo never silently does nothing. - Safe deletes. Deleting a project, registrar or registrar account also deletes what's under it, so it needs
?confirm=true. Without it you get a 409 that says what would be removed. - Dates.
YYYY-MM-DD. Timestamps are ISO 8601 in UTC. - CORS. Allowed from any origin, so browser tools work. Keep tokens out of public web pages.
Errors
Every error has the same shape:
response{
"error": {
"code": "invalid_request",
"message": "expiryDate: Use YYYY-MM-DD",
"details": [{ "field": "expiryDate", "message": "Use YYYY-MM-DD" }]
}
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A field is missing, has the wrong type, or the JSON doesn't parse. details lists each field. |
| 401 | unauthorized | No token, or the token is invalid, expired or revoked. |
| 403 | forbidden | A read-only token tried to make a change, or your project role doesn't allow it. |
| 404 | not_found | The thing doesn't exist or isn't yours. Both look the same on purpose. |
| 405 | method_not_allowed | The path exists but not with this method. The Allow header lists the ones that work. |
| 409 | conflict | The domain already exists in that registrar account. |
| 409 | confirmation_required | A delete that removes other records too. Repeat it with ?confirm=true. |
| 429 | rate_limited | Too many requests. Wait for the seconds in Retry-After. |
| 500 | internal_error | Something went wrong on our side. Retrying is safe for GET requests. |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
OpenAPI
The spec at https://lemon.domains/api/v1/openapi.json describes every endpoint and field. Import it into Postman, Insomnia or a client generator, or hand it to your AI assistant.