Limits & Restrictions
Rate limits
- `POST /api/auth/register` per-IP (agent channels — `mcp`/`a2a`/`form`): 3 signups per hour per source IP (Redis-tracked; fails open — i.e. does not block — if Redis is unreachable).
- Activation (any channel): after email confirmation, a corporate email + matching live website may auto-activate — capped at max 1 activation/hour platform-wide. Otherwise the account stays
PENDINGand requires a questionnaire + admin review.
429 response shape from the per-IP gate:
HTTP/1.1 429 Too Many Requests
{
"error": "rate_limited",
"message": "Too many signups from this address. Max 3 per hour."
}Field whitelist (request schema)
Every endpoint enforces a strict DTO whitelist via class-validator. Fields not declared on the DTO are rejected with 400.
Fields accepted by POST /api/auth/register:
email— required, valid email format.termsAccepted— required, must betrue.channel— optional, one ofweb | mcp | a2a | form(defaultweb).password— required forchannel=web(8–128 chars); optional for agent channels.firstName,lastName— optional, max 100 chars each.org— optional, max 200 chars.use_case— optional, max 500 chars.website— optional, max 300 chars.
Quotas
- Self-signup database quota: 50MB per tenant. Over quota,
POST/PUT/PATCHreturn403; reads andDELETEkeep working. - Agent key scopes: keys auto-issued at registration are limited — reads work immediately,
email:send/mailing:writeand other unavailable writes wait for activation — the account owner expands scopes in CRM Settings → API Keys once the workspace is active. - Starting credit: new self-signup workspaces may start with a small platform credit — check
GET /api/billing/balanceafter registering; the amount is configured platform-side and not fixed here.
Lifecycle limits (full-account flow)
- Email-confirmation link: expires in 24 hours.
- ToS-acceptance link: expires in 24 hours.
- Pending admin approval: auto-deleted after 7 days.
- Inactive registration: auto-deleted after 24 hours of inactivity.
Authentication brute-force
- All 401 cases (missing / malformed / unknown / revoked bearer) return the same generic
{"statusCode":401,"message":"Invalid credentials"}. Distinct messages would let attackers enumerate valid key formats. - Repeated 401s on protected endpoints share the general nginx
/v1/*burst-limit zone. Sustained brute-force is dropped at nginx with429. - Bearer tokens are stored as SHA-256 hashes — even with database read access, plaintext keys are not recoverable.
Backoff recipe
import time, random, requests
def call(url, **kwargs):
for attempt in range(6):
r = requests.post(url, **kwargs)
if r.status_code != 429:
return r
# Exponential backoff with jitter; signup window is 1 hour, so cap at 60s
time.sleep(min(60, 2 ** attempt) + random.uniform(0, 1))
raise RuntimeError("rate-limited after 6 retries")