flowsk.com
Reference

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.

Aug 4, 2026· 3 min read ·Docs
Quick answer

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 and sendBeacon works.
  • 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.

Read how it works

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.

Keep reading