Limits & Restrictions
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, then429.
Verified 429 response from the per-IP gate (not a generic message — by design informative for honest users):
HTTP/1.1 429 Too Many Requests
{
"statusCode": 429,
"message": "Too many signups from your address. Try again in an hour."
}Request body size
- `POST /v1/signup`: 64 KB. Larger bodies return
413from nginx before the application is reached. - **Other
/v1/*endpoints**: 1 MB.
# 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 LargeField whitelist (request schema)
Every endpoint enforces a strict DTO whitelist via class-validator with forbidNonWhitelisted: true. Any field not in the schema is rejected.
// 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 beform | 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.
// 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.
// 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 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")