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, then 429.

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 413 from 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 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.

// 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.
// 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 with 429.
  • 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")