# Limits & Restrictions

> **TL;DR for agents:** Signup: 5/IP/hour + 50 global/hour, body cap 64 KB. Password: min 10 chars + common-password blacklist. DTO whitelist on every field (extra fields = 400). Sandbox quota: 500/month. No `X-RateLimit-*` headers (anti-calibration).

## Rate limits

- **`POST /v1/signup` per-IP**: 5 successful signups per hour per source IP (Redis-tracked, key derived from `X-Forwarded-For`).
- **`POST /v1/signup` global ceiling**: 50 successful signups per hour across all IPs (anti-mass-registration safety net).
- **`POST /v1/signup` nginx burst**: 10 requests in burst window, then `429`.
- **Other `/v1/*` nginx burst**: 20 requests in burst window, then `429`.

Verified `429` response from the per-IP gate (not a generic message — by design informative for honest users):

```json
HTTP/1.1 429 Too Many Requests

{
  "statusCode": 429,
  "message": "Too many signups from your address. Try again in an hour."
}
```

> There are intentionally **no `X-RateLimit-*` response headers** — exposing remaining quota would help attackers calibrate brute-force attempts. Use exponential backoff with jitter when you receive a `429`.

## Request body size

- **`POST /v1/signup`**: 64 KB. Larger bodies return `413` from nginx before the application is reached.
- **Other `/v1/*` endpoints**: 1 MB.

```bash
# Verified: 70 KB body to /v1/signup -> 413
$ curl -X POST https://api.live-direct-marketing.online/v1/signup \
    -H 'Content-Type: application/json' \
    --data "{\"email\":\"x@x.com\",\"use_case\":\"$(python3 -c 'print("a"*70000)')\"}"
# HTTP/1.1 413 Request Entity Too Large
```

## Field whitelist (request schema)

Every endpoint enforces a strict DTO whitelist via `class-validator` with `forbidNonWhitelisted: true`. Any field not in the schema is rejected.

```js
// Verified: { "email":"x@y.com", "extra":"field" } on /v1/signup
{
  "statusCode": 400,
  "message": ["property extra should not exist"]
}
```

Per-field max lengths on `/v1/signup`:

- `email` — 254 chars (RFC 5321), valid email format required.
- `org` — 200 chars.
- `use_case` — 500 chars.
- `firstName`, `lastName` — 80 chars each.
- `password` — min 10 (service-enforced), max 200.
- `channel` — must be `form | a2a | mcp`.

## Password policy (full-account flow)

- **Minimum length**: 10 characters (service-enforced; DTO has a softer 8-char fallback for clearer error messages).
- **Common-password blacklist**: 30 well-known weak passwords (`password`, `qwerty123`, etc.) are rejected.
- **Maximum length**: 200 characters.

```js
// Verified: password "password123" -> 400
{
  "statusCode": 400,
  "message": "Password is too common. Please choose a stronger one."
}

// Verified: password "short" (<8) -> 400 (DTO layer)
{
  "statusCode": 400,
  "message": ["password must be longer than or equal to 8 characters"]
}
```

## Quotas

- **Sandbox monthly quota**: 500 messages per calendar month, resets at the 1st 00:00 UTC. Read live state via `GET /v1/me`.
- **Full-account quota**: configured per tenant on approval.

```js
// Verified: GET /v1/me on a fresh sandbox key
{
  "flow": "sandbox",
  "email": "agent@example.com",
  "scope": "sandbox",
  "moderation_status": "pending",
  "quota": {
    "monthly": 500,
    "used": 0,
    "remaining": 500,
    "resets_at": "2026-06-01T00:00:00.000Z"
  }
}
```

## 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 with `429`.
- Bearer tokens are stored as SHA-256 hashes — even with database read access, plaintext keys are not recoverable.

## Backoff recipe

```python
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")
```
