MemorySync
API Reference

Rate Limits & Errors

MemorySync enforces rate limits per API key, not per plan. When you create a key in Settings → API Keys, you pick one of four preset tiers or define your own. Each tier is a three-layer budget — per second, per minute, per hour — enforced atomically by a sliding-window limiter. Dashboard sessions and unauthenticated callers are limited separately, by IP and user.

Per-key tiers

Pick a tier when creating or editing an API key. The same numbers appear in the dashboard preset menu — they are the source of truth and applied to every request that key makes, regardless of route.

TierPer secondPer minutePer hour
free230100
pro102005,000
enterprise501,00050,000
custom(set by caller)(set by caller)(set by caller)
unlimited

How enforcement works

  • Every request with X-API-Key is identified as {environment}:apikey:{key_id}. The limiter looks up the key row, resolves its tier (or custom limits), and runs an atomic sliding-window check against the cache.
  • When any of the three layers is exceeded the gateway returns 429 with error.code = "RATE_LIMIT_EXCEEDED", blocked_by set to the layer that tripped, and a Retry-After header.
  • Custom limits are stored on the key as rate_limit_per_second and rate_limit_per_minute; the per-hour value is taken from the legacy rate_limit column.
  • Rotating a key keeps the tier; revoking a key voids its budget immediately.

Limits for non-key traffic

CallerLimitIdentifier
Dashboard session (JWT)20 / sec, 500 / min, 5,000 / houruser:{user_id}
Authenticated user (non-dashboard)10 / sec, 200 / min, 2,000 / houruser:{user_id}
Anonymous (per IP)10 / sec, 200 / min, 2,000 / hourip:{address}
Docs & health probes5 / sec, 60 / min, 600 / hourip:{address}
SCIM provisioning30 / sec, 1,000 / min, 50,000 / hourscim:{api_key_or_ip}

Per-route overrides

A small number of unauthenticated routes have their own per-IP caps that take precedence over the defaults above:

RouteLimit
POST /auth/login10 / minute / IP
POST /auth/signup5 / minute / IP
POST /auth/refresh60 / hour / principal

Response headers

  • X-RateLimit-Limit — the most-restrictive bucket size for the key.
  • X-RateLimit-Remaining — tokens left in the current window.
  • X-RateLimit-Reset — unix seconds when the bucket refills.
  • X-RateLimit-Per-Second-{Limit,Remaining,Reset}, -Per-Minute-*, -Per-Hour-* — per-layer breakdown so clients can see exactly which window will free up next.
  • Retry-After — seconds to wait, present on every 429.

Quota vs rate limit

Rate limits cap request throughput per key. Plan quotas cap monthly memory writes and queries on the organisation as a whole — they are enforced inside POST /memory/add and POST /memory/query after the rate-limit check. When a quota is exhausted, the API uses silent degradation: POST /memory/add returns 200 with {"status":"ok"} and the request is accepted but the memory is not stored; POST /memory/query returns 200 with {"memories":[]}. No error is surfaced to the caller. Watch the usage dashboard for metrics approaching 100% of your plan limit; webhook and email alerts can also be configured for at-quota and approaching-quota thresholds.

Error envelope

FieldNotes
error.codeStable machine-readable string. Branch on this in code.
error.messageHuman-readable explanation. Do not branch on this.
error.detailsObject with structured context (validation field, missing scope, blocked layer, …).
error.request_idTrace id. Quote this in support tickets.
error.timestampRFC 3339 UTC.
JSON
{
  "error": {
    "code": "validation_error",
    "message": "field 'text' is required",
    "details": { "field": "text" },
    "request_id": "req_01HX…",
    "timestamp": "2026-05-04T12:30:11Z"
  }
}

Common error codes

StatusCodeMeaning
400validation_errorRequest body or query parameters failed schema validation.
401unauthenticatedMissing or invalid bearer token / API key.
403forbiddenAuthenticated principal lacks required scope, role, or project access.
404not_foundTarget resource does not exist or is not visible to the caller.
429rate_limitedPer-IP or per-route limit exceeded — see Retry-After.
500internal_errorUnhandled server error. The request_id is logged for support.

Client strategy

  • Always retry 429 after the Retry-After seconds, never sooner.
  • Apply full jitter on retries; the official SDKs do this automatically.
  • Branch on error.code, never on error.message.
  • Quote error.request_id in support tickets — it correlates the request across audit, datastore, and worker logs.
  • Inspect error.details.blocked_by to see which layer (per-second / per-minute / per-hour) tripped, and back off proportionally.