Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.mareaalcalina.com/llms.txt

Use this file to discover all available pages before exploring further.

Rate limits

Marea uses per-key minute and day buckets with Stripe-compatible headers. Counts increment before the handler runs, so 4xx and 5xx responses still count against your quota — that’s deliberate, and it defends against spam-of-invalid-requests abuse.

Headers on every response

X-RateLimit-Limit: 60                ← rpm (requests-per-minute) for this key
X-RateLimit-Remaining: 47            ← remaining in the current minute window
X-RateLimit-Reset: 1714867260        ← epoch seconds when the minute rolls over
When you hit a limit, you also get:
HTTP/1.1 429 Too Many Requests
Retry-After: 23
Retry-After is in seconds. For minute-bucket overflow it’s seconds-until-the-next-minute (always ≤ 60). For day-bucket overflow it can be up to 86,400 seconds (a full day).
The headers report on the minute bucket only — there is no X-RateLimit-Remaining-Day header. Use GET /v1/me’s rateLimit.remainingDay for the day-bucket counter.

Default budgets

Read your live values from GET /v1/me (the rateLimit object). Values stored on the API key doc at issuance time:
Key prefixKey typerpmrpdNotes
mk_user_*User scope6010,000Issued via POST /v1/users/:userId/keys.
mk_dev_*Developer scope6050 (default)The dev-key rpd is the bootstrap-per-day ceiling — see below.
The developer key’s rpd is intentionally tiny. Developer keys exist to bootstrap accounts (POST /v1/users) and issue per-user keys (POST /v1/users/:userId/keys), not to drive sustained traffic. The rpd: 50 default is the bootstrap-per-day cap (configurable at issuance time per the IssueDeveloperKeyParams contract). Once an account is bootstrapped, route all subsequent traffic through that account’s mk_user_* key — that one gets the full 10,000/day budget.The middleware does not distinguish “this request is a bootstrap call” from “this request is something else” — it simply enforces whatever rpd is stored on the developer key. If you exhaust 50 requests on a dev key in a single day, you’re locked out of that dev key for the rest of the UTC day, full stop.
To check your remaining budget at any time, call GET /v1/me. The response includes:
{
  "rateLimit": {
    "rpm": 60,
    "rpd": 50,
    "remainingMinute": 60,
    "remainingDay": 47
  }
}
Header vs. body precision. The exact remainingMinute value lives in the response headers (X-RateLimit-Remaining). The /v1/me body’s remainingMinute is approximate — it’s the value at response-assembly time, not at your next request. For backoff logic, trust the headers.

429 response shape

{
  "error": {
    "type": "rate_limited",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded (rpm_exceeded). Retry after 23s.",
    "recoverable": true,
    "retryAfterMs": 23000,
    "nextActions": [
      { "label": "Wait 23s and retry the same request.", "method": null, "url": null }
    ]
  }
}
The message field tells you which bucket overflowed:
  • message contains rpm_exceeded → minute bucket. Wait Retry-After seconds (always ≤ 60), then retry.
  • message contains rpd_exceeded → day bucket. Retry-After is the seconds-remaining-in-the-day (up to 86,400). Do not retry tight; wait the indicated time or escalate to the user.
The retryAfterMs field on the error body mirrors Retry-After in milliseconds. Both recoverable: true and nextActions[0].label indicate that the call is safe to repeat once the window opens. Minute-bucket overflow:
attempt 1: hit 429, message contains "rpm_exceeded", Retry-After: 23
                                                  → sleep 23s, retry
attempt 2: succeed (200)
Day-bucket overflow:
attempt 1: hit 429, message contains "rpd_exceeded", Retry-After: 41200
                                                  → surface to user:
                                                    "Daily quota reached.
                                                     Try tomorrow or upgrade."
                                                  → DO NOT auto-retry
When using Idempotency-Key, keep the same key across retries — the server treats the eventual successful call as a replay-safe completion of the original request. See Safe mutations.

What counts against your quota

Every authenticated request to a /v1 endpoint — 200, 4xx, 5xx — counts. The increment happens before the handler runs, so a malformed body that the handler would have rejected with a 422 still consumes one minute-bucket slot and one day-bucket slot. Exceptions:
  • Unauthenticated requests (rejected at the apiKey middleware before rate-limit increments) do not count.
  • The health-check shortcut at /healthz does not pass through the rate-limit middleware.
  • If the rate-limit Firestore transaction itself fails (infrastructure error), the request is failed open — it proceeds without an increment and without a 429. A structured log entry is emitted so the 5xx-rate alert surfaces the underlying infra failure.

Conventions

Stripe-compatible headers and semantics — X-RateLimit-* plus Retry-After. The same backoff library you’d use for Stripe works here unchanged. Buckets are per key, not per account. If a single user has both mk_user_keyA and mk_user_keyB, each gets its own 60/10,000 budget. This is by design: a misbehaving integration on one key can’t starve a well-behaved integration on another key for the same user.