Menu

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.

Base URL
https://lemon.domains/api/v1
Auth
Authorization: Bearer <token>
Format
JSON requests and responses
Rate limit
300 requests a minute per 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

terminal
curl 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

terminal
curl "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

terminal
curl -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 /domains takes limit (up to 500) and offset, and returns total and hasMore.
  • Status. displayStatus is 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; pass archived=include or archived=only to GET /domains to see them.
  • Look up by name. GET /domains/example.com works as well as GET /domains/<id>.
  • Partial updates. PATCH changes 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" }]
  }
}
StatusCodeWhen
400invalid_requestA field is missing, has the wrong type, or the JSON doesn't parse. details lists each field.
401unauthorizedNo token, or the token is invalid, expired or revoked.
403forbiddenA read-only token tried to make a change, or your project role doesn't allow it.
404not_foundThe thing doesn't exist or isn't yours. Both look the same on purpose.
405method_not_allowedThe path exists but not with this method. The Allow header lists the ones that work.
409conflictThe domain already exists in that registrar account.
409confirmation_requiredA delete that removes other records too. Repeat it with ?confirm=true.
429rate_limitedToo many requests. Wait for the seconds in Retry-After.
500internal_errorSomething 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.