API Integration documentation

Rate limiting

The thresholds per endpoint, and how to handle them properly.

Each public endpoint has a threshold, counted per minute and per real IP address — the visitor's, not the proxy's.

Endpoint Limiter Per minute and per IP Per hour and per website
POST …/track api-track 60 50 000
GET …/signature api-signature 30
GET …/captcha/altcha/challenge api-captcha-challenge 30
GET …/forms/config api-forms-config 60
POST …/submit api-submit depends on the client's plan, 5 by default 1 000

Submission limiting is counted per IP and per website: two visitors behind the same internet gateway, on the websites of two different clients, do not share the same bucket.

The two hourly ceilings

Submissions and tracking carry a second counter, per website and per hour, all IP addresses together: a flood spread over many addresses passes the per-IP threshold, but not that one.

Counter Default Environment variable
Submissions, per website and per hour 1 000 PLATFORM_SUBMISSIONS_PER_WEBSITE_PER_HOUR
Tracking, per website and per hour 50 000 PLATFORM_TRACKS_PER_WEBSITE_PER_HOUR

They are deliberately wide: a website that reaches them is under attack, not busy. The submission counter is incremented before the captcha is verified — a tighter ceiling would let a third party block a client's leads. Either counter answers a plain 429, like the per-minute thresholds.

IPv6 counts by /64

A bucket is the IPv4 address, or, for an IPv6 address, its /64 prefix — a single subscriber is given a whole /64, and changing address inside it must not reset their counter. 2001:db8:1:2:3:4:5:6 and 2001:db8:1:2::ff share the bucket 2001:db8:1:2::/64. This applies to every limiter above.

The address itself comes from CF-Connecting-IP, and only when the connection actually arrives from a Cloudflare range; otherwise it is the address of the connection. A forged header therefore buys nothing.

429 response

HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0

Honor Retry-After: it is the number of seconds before the next useful attempt. Retrying earlier only extends the block.

Two barriers, not one

These thresholds are the fine-grained barrier, enforced by the application. A broader barrier is enforced upstream, at the network edge, with deliberately more permissive thresholds: it absorbs floods without waking the application.

In practice: a burst of requests from a single address may be blocked at the edge, and the response is then not the application's JSON but a block page. A fetch() that receives HTML where it expects JSON should look at that barrier.

Behaving well

  • Only request a signature when you need one: it is valid for ten minutes.
  • Only call tracking once per page view.
  • Keep your API key in configuration: it is a long-lived secret, not something to fetch.
  • On 429, wait; on 403, fix the cause — a 403 is never solved by insisting.