Limits & abuse protection
The signup endpoint is deliberately anonymous-write: any visitor on any origin can post to it, the project ID is public, and there's no credential. That's the only way a signup form on someone else's site can work.
That design is safe on its own — creating a pending subscriber isn't privileged, and
double opt-in means nobody can be subscribed against their will. What it does expose is
the mail sender: every accepted request can put an email in someone's inbox. These
limits bound that.
Confirmation resend cooldown
One confirmation email per address per 15 minutes, however many times signup is requested.
Without this, repeatedly submitting one address would mail that person without limit — an email-bombing amplifier pointed at a victim, sent from your domain.
The response is identical whether or not an email was actually sent, so the endpoint can't be used to probe which addresses are already pending. The signup is still recorded either way.
This applies to POST /api/v1/subscribers too. If you're importing a
list and re-running the import, the second run won't re-mail everyone.
Request rate limits
Applied per identifier on the public signup endpoint:
| Scope | Limit |
|---|---|
| Per IP address | 20 / hour |
| Per email address | 5 / hour |
| Per project | 500 / hour |
Exceeding one returns 429 with a Retry-After header:
{ "error": "Too many signup attempts. Try again shortly." }Surface that as "try again shortly" — don't retry in a loop.
These windows are generous enough that ordinary use never trips them: someone subscribing twice, or an office sharing one NAT egress IP, stays well under. They exist to stop scripted floods.
The rate limiter fails open — if its datastore is unreachable, requests are allowed rather than rejected. It's a mitigation, not a hard gate. The cooldown above is the durable protection, because it's enforced by stored state rather than a counter.
Address validation
Addresses are validated strictly before anything is sent, rejecting whitespace, doubled or trailing dots, and single-character TLDs.
This is deliverability protection more than abuse protection: every accepted junk address becomes a bounce, and bounces cost sending reputation for every site sending from your domain.
Addresses over 254 characters (the RFC 5321 limit) are rejected.
Signed click tracking
Click-tracking links are HMAC-signed against the exact destination, campaign, and subscriber. A link with a missing or tampered signature is rejected instead of followed.
Without the signature, the redirect would forward to any URL supplied in the query string — turning your sending domain into a phishing hop. Signing means it only ever forwards to a URL Inkform itself put in that campaign.
Click links in campaigns sent before this shipped carry no signature and now return
400. That's deliberate — honouring them would mean continuing to serve an open
redirect.
What isn't limited
Confirm, unsubscribe, and tracking endpoints have no request rate limit. They're protected by unforgeable HMAC tokens instead, compared in constant time, with distinct purpose prefixes so a token for one action can't be replayed as another.
Adding your own protection
The limits above bound damage; they don't stop a determined, distributed attacker from submitting plausible addresses. If your signup form is being abused:
- Put Cloudflare Turnstile or hCaptcha in front of the form and verify the token server-side before forwarding the signup.
- Proxy through your own backend rather than posting from the browser, so you can apply your own rules and keep the project ID out of the page.
- Watch signup
sourcelabels in the dashboard — a flood usually shows up as one source spiking.