The events API
One endpoint, one batch format. Every event carries a source (client or server), a category (navigation, identification, purchase) and an optional de-duplication key.
POST /api/events { write_key, anonymous_id, events: [...] }
One endpoint for everything. The anonymous_id always travels in the payload, never as a cookie — which is why no browser's third-party cookie policy has any bearing on whether attribution works.
Max 50 events per batch. Server-to-server callers post application/json; the snippet posts text/plain to avoid a CORS preflight.
Endpoint
POST https://flowsk.com/api/events
Authentication is the property’s public write_key, either in the JSON body or as a query parameter. It is write-only and safe to expose.
Request
{
"write_key": "pk_live_…",
"anonymous_id": "f9fc2575-8339-4573-9ec9-02d2a2282f5c",
"events": [
{
"source": "client | server", // default: client
"category": "navigation | identification | purchase", // default: navigation
"name": "$view", // allow-listed, or a $system name
"dedup_key": "order_10482", // optional idempotency / merge key
"email": "hello@example.com", // identification and purchase
"value_cents": 12900, // purchase
"url": "https://…", // navigation
"referrer": "https://…", // navigation
"properties": { "plan": "pro" } // free-form, bounded
}
]
}
Response
{ "ok": true, "processed": 1 }
The three categories
| Category | Meaning | Becomes |
|---|---|---|
navigation |
Behaviour on a page: pageview, click, submit, tool use | A journey event |
identification |
You learned who the person is | A person, plus a journey event |
purchase |
Money moved | A conversion, plus a journey event |
An identification event without an email, or a purchase without a value, is still recorded — as a behavioural funnel event. checkout_started is a real signal even though nothing was paid.
Source, and why it matters
source is client or server. It is not metadata; it is how the system knows what to trust.
client— sent by the browser. Rich context, real-time, blockable.server— sent by your backend. Authoritative, unblockable, and it carries the cause of an event, which the browser cannot know.
When both report the same fact with the same dedup_key, they merge and the record’s source becomes both.
Server-side examples
A purchase, from your payment webhook:
curl -X POST https://flowsk.com/api/events \
-H "Content-Type: application/json" \
-d '{
"write_key": "pk_live_…",
"anonymous_id": "f9fc2575-8339-4573-9ec9-02d2a2282f5c",
"events": [{
"source": "server",
"category": "purchase",
"name": "purchase",
"email": "hello@example.com",
"value_cents": 12900,
"dedup_key": "order_10482"
}]
}'
An identification, with the cause recorded:
{
"source": "server",
"category": "identification",
"name": "signup_started", // ← the action that caused it
"email": "hello@example.com",
"dedup_key": "user_4821"
}
That name is the thing a client-side identify() call can never supply. “Identified via newsletter” and “identified via demo_requested” describe completely different intent, and only your backend knows which one happened.
Limits
| Limit | Value |
|---|---|
| Events per batch | 50 |
| Requests per minute, per write key | 240 |
| Events per minute, per property | 6,000 |
| Daily quota | Per plan; over quota drops silently with ok: true |
Silent dropping over quota is intentional. An error at that boundary would surface as a broken page on a customer’s site, and no measurement is worth that.
Content types
- Browser:
text/plain;charset=UTF-8— makes the POST a CORS simple request, so there is no preflight andsendBeaconworks. - Server:
application/json.
Both accept the same body. A proxy-mode backend can post a mixed batch where some events carry source: "client" and others source: "server"; each is classified on its own.
The id travels in the payload
Worth restating, because it is the design decision everything else depends on: anonymous_id is sent in the request body, not as a cookie the API reads.
That means no third-party cookie policy in any browser affects whether attribution works. The only thing that varies between install modes is who owns the id — and therefore how long it survives. See proxy setup.
Frequently asked questions
Why does the snippet post text/plain?
So the POST qualifies as a CORS simple request. No preflight round trip, and navigator.sendBeacon works — which is what lets an event survive the page being closed.
What happens if I exceed the rate limit?
You get a 429. The limits are 240 requests per minute per write key and 6,000 events per minute per property. Over the daily quota, events are dropped silently with ok: true — tracking must never break a customer's page.
Can I send events for a visitor I have no anonymous_id for?
Yes for purchases and identifications, by email — they attach to the person. But the journey before that point will not be joined, so pass the anonymous id whenever you have it.
Are unknown event names rejected?
Names outside the allow-list are not recorded. This is deliberate: a typo in a tracking call should not silently create a new event type that nobody notices for a quarter.
How do I test without polluting my data?
Use a separate property with its own write key for staging. Properties are free and fully isolated.
See the whole contract in one page
How it works walks through the three install modes, the client/server split and de-duplication with worked examples.
Stop guessing which ad made the sale.
Flowsk Signals stitches the anonymous click to the email to the purchase — first-party, server-side, de-duplicated. One snippet, $29/mo, and every conversion comes with a receipt you can inspect.