Authentication

Every Leto SEO API request — REST and MCP — is authenticated with a personal API key passed as a Bearer token. This page explains how to generate keys, make requests, understand rate limits, and handle errors.

Generating an API key

API keys are created from the Settings page inside the Leto SEO app (/settings). Navigate to the API keys section and click Generate key.

Keys are shown once. Copy the full key immediately after generation — it is hashed with SHA-256 before storage and cannot be retrieved again. If you lose a key, revoke it and generate a new one.

All keys have the prefix sk_live_ followed by a 32-character hex string. Keys are tied to your account (user ID), not to a specific site.

Making authenticated requests

Pass your API key in the Authorization header using the Bearer scheme on every request:

bash
Authorization: Bearer sk_live_<your-key>

The same header format applies to both the REST v1 API and the MCP server. Here are examples in three languages:

bash
curl -X GET https://letoseo.com/api/v1/portfolio \
  -H "Authorization: Bearer sk_live_<your-key>"

Owner gating

A valid API key can still be rejected with a 403 if the key’s owner account is not yet approved or is in the process of being deleted. This lets you distinguish a wrong key (401) from a right key whose owner is gated (403).

Two distinct error bodies are returned:

403Account pending approval — signup is awaiting manual review.
json
{
  "error": "owner_pending_approval"
}
403Account deletion in progress — the owner has requested account deletion.
json
{
  "error": "owner_deletion_pending"
}

Source: src/lib/api/with-api-auth.ts:34–45

Rate limits

Rate limits are enforced with sliding windows keyed on the API key owner’s user ID — not on the individual key. If you create multiple keys for the same account they all draw from the same quota.

PoolLimitWindowApplies to
api:hr500 requests1 hourREST v1 + MCP (shared)
api:min30 requests1 minuteREST v1 + MCP (shared)
mcp:min10 requests1 minuteMCP only (additional cap)

MCP requests are subject to all three limits simultaneously — the two shared limits plus the MCP-specific per-minute cap. REST v1 requests are subject only to the two shared limits.

When a limit is exceeded, the API returns a 429 with the following body and headers:

429Rate limit exceeded.
json
{
  "error": "rate_limit_exceeded",
  "retryAfterSeconds": 42
}
HeaderValue
Retry-AfterSeconds to wait before retrying (integer)
X-RateLimit-RemainingAlways 0 on a 429
X-RateLimit-ResetUnix timestamp (ms) when the window resets

Source: src/lib/ratelimit/apply.ts:69–91, src/app/api/mcp/[transport]/route.ts:530–538

REST error catalogue

All error responses are JSON with a top-level error field. Successful responses use 200 OK for reads and updates, 201 Created for creates, and 204 No Content for empty-body deletes.

401Missing or invalid API key.
json
{
  "error": "Invalid or missing API key. Use Authorization: Bearer sk_live_..."
}
400Bad request — validation failed. Exact message varies by endpoint.
json
{
  "error": "Missing required field: keywords"
}
403Forbidden — key is valid but the owner account is gated. See Owner gating above.
json
{
  "error": "owner_pending_approval"
}
404Resource not found, or the requesting user does not own the resource.
json
{
  "error": "Site not found"
}
429Rate limit exceeded. See Rate limits above for headers.
json
{
  "error": "rate_limit_exceeded",
  "retryAfterSeconds": 42
}
500Unexpected server error. Retry with exponential back-off.
json
{
  "error": "Internal server error"
}

201 Created is returned by POST /api/v1/sites/:siteId/keywords and POST /api/v1/sites/:siteId/campaigns.

204 No Content (empty body) is returned only by DELETE /api/v1/sites/:siteId/keywords. The campaign delete endpoint (DELETE /api/v1/sites/:siteId/campaigns/:campaignId) returns 200 { ok: true } instead.

MCP error behavior

The MCP transport layer does not have a native equivalent of HTTP status codes. In the current V1 implementation, both rate-limit failures and owner-status rejections (pending / deletion_pending) collapse into a single MCP authentication failure — clients see an auth error rather than a distinct rate-limit or forbidden response.

This asymmetry is intentional for V1 and is called out here so integrators know to interpret an MCP auth failure as potentially rate-related, not only key-related. A more granular error surface will be added in a future release.

Source: src/app/api/mcp/[transport]/route.ts:530–545

CORS

The REST v1 API is CORS-enabled for all origins. Every response includes the following headers:

HeaderValue
Access-Control-Allow-Origin*
Access-Control-Allow-MethodsGET, POST, PATCH, DELETE, OPTIONS
Access-Control-Allow-HeadersAuthorization, Content-Type

PATCH is included in Access-Control-Allow-Methods because browser clients using PATCH /api/v1/sites/:siteId/campaigns/:campaignId require it to pass preflight. Source: src/lib/api/with-api-auth.ts:76–82

API key revocation

Revocation is instant. When you delete a key in /settings, the database marks it with a revoked_at timestamp. The resolution RPC (resolve_api_key_owner_status) filters on revoked_at IS NULL, so the very next request with the revoked key returns a 401.

There is no grace period or cache invalidation step. Revoke a key and it is gone immediately.

Source: src/lib/auth/api-key.ts:63–70